Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/iterate/sqlfu/llms.txt

Use this file to discover all available pages before exploring further.

Durable Objects give each instance its own private SQLite database. A code deploy updates the Worker bundle, but existing Durable Object storage stays whatever that instance has applied so far. sqlfu handles this by running migrate() synchronously in the constructor — before the first request is served.

Project shape

Give each Durable Object class its own sqlfu project:
src/durable-objects/counter/
├── definitions.sql
├── migrations/
│   └── 20260506000000_create_counter.sql
├── sql/
│   ├── queries.sql
│   └── .generated/
├── sqlfu.config.ts
└── counter.ts
If you have several Durable Object classes with different schemas, give each one its own folder and pass --config explicitly:
npx sqlfu --config src/durable-objects/counter/sqlfu.config.ts draft
npx sqlfu --config src/durable-objects/counter/sqlfu.config.ts generate

Config

Durable Object storage is not available to the Node CLI as a simple local file, so the typical Durable Object config omits db. sqlfu uses .sqlfu/app.db as a local authoring database for commands that need one, while draft and generate still read from definitions.sql and migration files.
sqlfu.config.ts
import {defineConfig} from 'sqlfu';

export default defineConfig({
  definitions: './definitions.sql',
  migrations: './migrations',
  queries: './sql',
  generate: {
    sync: true,
  },
});
generate.sync: true matters because Durable Object SQLite is synchronous. Generated query wrappers accept a SyncClient and return rows directly instead of promises.

Author schema and queries

1

Write your schema

Author the schema in definitions.sql:
definitions.sql
create table counters (
  name text primary key not null,
  value integer not null default 0
);
2

Write your queries

Author runtime queries in sql/queries.sql:
sql/queries.sql
/** @name incrementCounter */
insert into counters (name, value)
values (:name, 1)
on conflict (name) do update set value = value + 1
returning name, value;
3

Draft, generate, and commit

Run the standard sqlfu commands:
npx sqlfu --config src/durable-objects/counter/sqlfu.config.ts draft
npx sqlfu --config src/durable-objects/counter/sqlfu.config.ts generate
Commit the reviewed migrations/*.sql files. The generated migration bundle at migrations/.generated/migrations.ts is what the Durable Object imports at runtime.

Durable Object runtime

Import migrate from the generated module and call it in the constructor:
counter.ts
import {DurableObject} from 'cloudflare:workers';
import {createDurableObjectClient} from 'sqlfu';

import {migrate} from './migrations/.generated/migrations.ts';
import {incrementCounter} from './sql/.generated/queries.sql.ts';

type Env = {};

export class CounterObject extends DurableObject {
  client: ReturnType<typeof createDurableObjectClient>;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);

    this.client = createDurableObjectClient(ctx.storage);
    migrate(this.client);
  }

  fetch(request: Request) {
    const url = new URL(request.url);
    const name = url.searchParams.get('name') || 'default';
    const row = incrementCounter(this.client, {name});

    return Response.json(row);
  }
}
Pass ctx.storage, not ctx.storage.sql. The full storage object lets sqlfu use Durable Objects’ transactionSync() API so each migration is applied inside a real storage transaction with per-migration rollback on failure. If you only need ad-hoc query access without migrations, pass {sql: ctx.storage.sql} explicitly.

How runtime migrations work

migrate() is synchronous and idempotent. It checks the sqlfu_migrations table in that object’s private SQLite database, skips migrations already recorded there, and applies only the files that are missing from the bundle. Because the Durable Object migrator is synchronous, calling migrate(this.client) directly in the constructor is enough — the object does not serve a request before the constructor returns, so no await or deferred initialization is needed.

What happens if an applied migration is missing

sqlfu treats a missing applied migration as a migration-history integrity problem. If a Durable Object instance has recorded sqlfu_migrations rows for migrations that are no longer present in the generated bundle, migrate() fails with a deleted-applied-migration error before applying any newer migrations.
This is intentional. sqlfu does not synthesize missing SQL at runtime. Runtime schema changes still need reviewable migration files because renames, destructive changes, and backfills are product decisions. Use sqlfu draft to turn definitions.sql changes into migration files before deploying.

Generated migration bundle

sqlfu generate emits migrations/.generated/migrations.ts when migrations is configured. This file is a plain TypeScript bundle containing your migration SQL as string literals — no filesystem reads at runtime. Commit both migrations/*.sql and the generated bundle. The migrate export is what your Durable Object calls.

Migration model

The four authorities, pending migrations, history drift, and what each command mutates.

Cloudflare D1 guide

Connect sqlfu to a deployed D1 database for CLI and UI access.

Adapters reference

The Durable Object adapter snippet and full compatibility matrix.

Getting started

The base SQL → draft → generate workflow.

Build docs developers (and LLMs) love