Skip to content
Discord Get Started

Browser SDK

The @db9/browser SDK lets you query DB9 databases directly from browser and edge environments. All queries flow through the public data API with Row-Level Security (RLS) enforcement — you never expose raw connection strings or admin credentials to the client.

ScenarioRecommended tool
Client-side data access with per-user RLSBrowser SDK@db9/browser
Server-side provisioning, migrations, fs9Node.js SDKget-db9 (TypeScript SDK)
Terminal database managementCLIdb9 (CLI Reference)
ORM or driver connection from a backendRaw pgwire — use the connection string (Connect)

Works in any environment with fetch — browsers, Deno, Cloudflare Workers, Next.js client components.

Terminal
npm install @db9/browser

Also available via pnpm add @db9/browser, yarn add @db9/browser, or bun add @db9/browser.

Before using the browser SDK you need:

  1. A database — create one with db9 create or the Node.js SDK.
  2. A publishable key — create one via the REST API (see Security & Auth — Publishable Keys).
  3. (Optional) Auth config — if you want per-user RLS, configure BYO JWT on the database (see Security & Auth — BYO JWT).
TypeScript
import { createDb9BrowserClient } from '@db9/browser';
const db9 = createDb9BrowserClient({
apiUrl: 'https://api.db9.ai',
databaseId: 'your-database-id',
anonKey: 'db9pk_your_publishable_key',
});
// Read rows
const { data, error } = await db9.from('todos').select('*');
if (error) {
console.error(error.message, error.statusCode);
} else {
console.log(data); // [{ id: 1, task: '...', done: false }, ...]
}

Creates a client instance. The client is stateless — no persistent connections are opened until a query executes.

TypeScript
function createDb9BrowserClient(options: Db9BrowserClientOptions): Db9BrowserClient;
OptionTypeRequiredDescription
apiUrlstringYesBase URL of the DB9 API (e.g., https://api.db9.ai).
databaseIdstringYesTarget database ID.
anonKeystringYesPublishable key (db9pk_...). Safe to embed in client-side code.
fetchtypeof fetchNoCustom fetch implementation (e.g., for testing or polyfills).
timeoutnumberNoRequest timeout in milliseconds.

Without authentication, queries run under the anonymous role. To enable per-user RLS enforcement, attach a JWT from your auth provider.

Set a token you already have (e.g., after login):

TypeScript
db9.auth.setSession({ accessToken: userJwt });

Supply an async function that is called before each request — ideal for expiring tokens:

TypeScript
db9.auth.setAccessTokenProvider(async () => {
return await myAuthService.getAccessToken();
});
TypeScript
const session = db9.auth.getSession();
// { accessToken: '...' } or null
db9.auth.clearSession(); // reverts to anonymous role
MethodDescription
setSession({ accessToken })Set a static JWT token.
setAccessTokenProvider(fn)Set an async token provider called before each request.
getSession()Returns { accessToken } or null.
clearSession()Removes token and provider; reverts to anonymous role.

All queries start with db9.from(table), which returns a TableRef. From there, chain into one of four operations: select, insert, update, or delete.

Every query returns Db9Result<Row[]> — an object with { data, error }. The SDK never throws on API errors; check error instead.

TypeScript
type Row = Record<string, unknown>;
interface Db9Result<T> {
data: T | null;
error: Db9BrowserError | null;
}
TypeScript
// All columns, all rows (up to server default limit of 100)
const { data } = await db9.from('todos').select('*');
// Specific columns
const { data } = await db9.from('todos').select('id', 'task');
// With filters, ordering, and pagination
const { data } = await db9
.from('todos')
.select('*')
.eq('status', 'active')
.order('created_at', 'desc')
.limit(20)
.offset(40);
MethodSignatureDescription
select(...columns: string[])Columns to return. '*' or omit for all.
eq(column, value)column = value
neq(column, value)column != value
gt(column, value)column > value
gte(column, value)column >= value
lt(column, value)column < value
lte(column, value)column <= value
like(column, pattern)column LIKE pattern
ilike(column, pattern)column ILIKE pattern (case-insensitive)
in(column, values[])column IN (...)
is(column, null | boolean)column IS NULL / column IS NOT NULL
filter(column, op, value)Generic filter with any FilterOperator.
order(column, 'asc' | 'desc')Sort results. Default: 'asc'. Chainable for multi-column sort.
limit(count)Max rows to return (server max: 1000).
offset(count)Skip rows for pagination.
TypeScript
const { data, error } = await db9
.from('todos')
.insert({ task: 'Ship browser SDK', status: 'active' })
.returning('*');
MethodSignatureDescription
values(record)Set column values. Also accepted as argument to insert().
returning(...columns)Columns to return from the inserted row. '*' for all.
TypeScript
const { data, error } = await db9
.from('todos')
.update({ status: 'done' })
.eq('id', 42);
MethodSignatureDescription
set(record)Set column values. Also accepted as argument to update().
eq(column, value)Filter rows to update.
filter(column, op, value)Generic filter.
TypeScript
const { data, error } = await db9
.from('todos')
.delete()
.eq('id', 42);
MethodSignatureDescription
eq(column, value)Filter rows to delete.
filter(column, op, value)Generic filter.

To query tables outside the public schema, use dot notation:

TypeScript
const { data } = await db9.from('analytics.events').select('*');

The schema must be listed in the publishable key’s exposed_schemas.

The SDK returns errors in Db9Result.error instead of throwing. The error object includes a human-readable message and the HTTP status code.

TypeScript
const { data, error } = await db9.from('todos').select('*');
if (error) {
console.error(`[${error.statusCode}] ${error.message}`);
// e.g., [403] "table 'secrets' is not exposed by this key"
// e.g., [401] "invalid or expired publishable key"
}
TypeScript
class Db9BrowserError extends Error {
readonly statusCode: number;
}
Status CodeMeaning
400Invalid request (bad filter, missing values).
401Invalid publishable key or expired JWT.
403Table or schema not exposed by the key, or RLS denied access.
404Database not found.
429Rate limit exceeded (configure via publishable key).

All types are exported from the package entry point:

TypeScript
import type {
Db9BrowserClientOptions,
Db9BrowserClient,
Db9Auth,
AccessTokenProvider,
QueryOperation,
FilterOperator,
QueryFilter,
QueryOrder,
QueryRequest,
QueryResponse,
ColumnInfo,
QueryError,
Db9Result,
Row,
} from '@db9/browser';
import {
createDb9BrowserClient,
Db9BrowserError,
SelectBuilder,
InsertBuilder,
UpdateBuilder,
DeleteBuilder,
TableRef,
} from '@db9/browser';
TypeScript
// lib/db9.ts
import { createDb9BrowserClient } from '@db9/browser';
export const db9 = createDb9BrowserClient({
apiUrl: process.env.NEXT_PUBLIC_DB9_API_URL!,
databaseId: process.env.NEXT_PUBLIC_DB9_DATABASE_ID!,
anonKey: process.env.NEXT_PUBLIC_DB9_ANON_KEY!,
});
TypeScript
// components/TodoList.tsx
'use client';
import { useEffect, useState } from 'react';
import { db9 } from '../lib/db9';
import { useAuth } from '../hooks/useAuth';
export function TodoList() {
const { jwt } = useAuth();
const [todos, setTodos] = useState([]);
useEffect(() => {
if (jwt) {
db9.auth.setSession({ accessToken: jwt });
} else {
db9.auth.clearSession();
}
}, [jwt]);
useEffect(() => {
async function load() {
const { data, error } = await db9
.from('todos')
.select('*')
.order('created_at', 'desc')
.limit(50);
if (data) setTodos(data);
}
load();
}, [jwt]);
return (
<ul>
{todos.map((t) => (
<li key={t.id}>{t.task}</li>
))}
</ul>
);
}