Create a database and use it from your Worker
Use your existing Worker or service. Creating a database needs a schema; it needs neither application code nor a base file.
1. Install and connect
r2db is in private preview: accounts are limited to invited email addresses, and signup answers 403 for an address that is not on the list. Create an account in the console — an invited account may start with trial credits already on its balance, otherwise purchase credits — and create an API token. Keep the account token in your server or Worker secrets.
The SDK and CLI are the same package. Until npm publication, build a preview tarball from the repository:
pnpm install
pnpm --filter r2db pack --pack-destination /tmp/r2db-release
npm install /tmp/r2db-release/r2db-0.1.0.tgz
npx r2db login --token-stdin
Login reads the token from stdin until EOF and targets https://r2db.dev; pass --api <origin> to reach another deployment. For automation, use R2DB_API_URL and R2DB_TOKEN as secrets.
2. Declare your schema
Create schema.ts:
import { table, createSchema, string, boolean } from 'r2db';
export const task = table('task')
.columns({ id: string(), title: string(), done: boolean() })
.index('task_by_done', 'done')
.primaryKey('id');
export const schema = createSchema({ tables: [task] });
Declare an index for the filters and ordering you will use. An uncovered query can scan object storage and exceed its request budget as data grows. Query advice helps identify this before production.
3. Create the database
npx r2db create --name tasks --schema ./schema.ts
The command prints the database ID and API endpoint. Repeating the same name and schema resumes an interrupted creation and returns the same database. A different schema under an existing name is refused; creation never replaces data.
Set R2DB_DATABASE_ID to the printed ID and R2DB_API_URL to the service origin in your Worker's environment.
4. Read and write from your Worker
import { createClient, newQueryBuilder, isoTx } from 'r2db';
import { schema } from './schema';
const q = newQueryBuilder(schema);
export default {
async fetch(request, env) {
const db = createClient({
url: env.R2DB_API_URL,
token: env.R2DB_TOKEN,
databaseId: env.R2DB_DATABASE_ID,
schema,
});
if (request.method === 'POST') {
const input = await request.json();
// Validate input and authorize the caller in your application.
await db.write([
isoTx.insert('task', { id: input.id, title: input.title, done: false }),
], { idempotencyKey: input.operationId });
}
return Response.json(await db.exec(q.task.where.done(false).limit(50)));
},
};
Each write batch commits atomically. The SDK retries transient failures with the same idempotency key. Give a logical operation a stable operationId when it may be retried across separate application requests. Receipts are retained for 24 hours. See write outcomes.
5. Keep a query live
In a service or request with an appropriate connection lifetime:
const live = await db.subscribe(q.task.where.done(false).limit(50));
console.log(live.data); // The initial result is already loaded, including an empty result.
const stop = live.subscribe(rows => console.log(rows));
live.onError(error => console.error(error));
// When this consumer is finished:
stop();
live.close();
The SDK receives the initial snapshot and committed changes, renews authorization on reconnect, and replaces the result with a fresh snapshot after a gap. It keeps the previous rows visible during recovery; inspect live.status for live, reconnecting, error, or closed. Pass a request AbortSignal to db.subscribe(query, { signal }) to release it when that request is canceled.
The account token belongs on your server. Your Worker owns authentication and delivery to browser clients.
Optional: run code beside the database
npx r2db deploy --database YOUR_DATABASE_ID --entry ./src/server.ts
This attaches an application facet to the existing database without replacing its data. Facet transactions support synchronous branching logic; their callbacks may replay. HTTP calls continue to work.