Skip to content
pre-alpha. The TypeScript API may still shift. The SQL won't.

This walkthrough builds a small posts app from scratch: schema in SQL, migrations drafted automatically, typed TypeScript wrappers generated from your query files. By the end you will have a working getPosts(client, {limit: 10}) call with full IDE types.

If you want to see the finished project running in your browser first, open the demo. It uses the same schema and queries with no install.

.
├── .sqlfu/
│ └── app.db
├── definitions.sql
├── migrations/
│ └── 20260101000000_add_posts_table.sql
├── sql/
│ ├── .generated/
│ │ └── queries.sql.ts
│ └── queries.sql
└── sqlfu.config.ts
Terminal window
pnpm add sqlfu

Optionally install the CLI globally so sqlfu is on your PATH anywhere:

Terminal window
npm install -g sqlfu

When you run sqlfu inside a project that already has it as a dependency, the global binary delegates to the project-local copy (node_modules/sqlfu). Each project runs the version it pinned. Your global install never has to match, and upgrading globally is not required to unstick a project.

Terminal window
npx sqlfu init

(If you just run npx sqlfu with no config, it will prompt you to init first. sqlfu init makes it explicit.)

This creates sqlfu.config.ts, definitions.sql, migrations/, sql/, and a .gitignore entry for .sqlfu/. The config already points at sensible defaults:

sqlfu.config.ts
export default {
migrations: './migrations',
definitions: './definitions.sql',
queries: './sql',
};

The config deliberately omits db: commands that need a local database will use .sqlfu/app.db until you point sqlfu at your app’s real runtime database.

Open definitions.sql and describe the schema you want right now:

create table posts (
id integer primary key autoincrement,
slug text not null unique,
title text not null,
body text not null,
published integer not null default 0
);

definitions.sql is the single source of truth for your desired schema. When you change it, sqlfu computes the diff and writes the next migration for you.

Terminal window
npx sqlfu draft

sqlfu replays your migration history into a scratch database, diffs it against definitions.sql, and writes a new migration file under migrations/. Open the file and review it — the diff engine is not psychic, so check for renames and destructive changes.

The generated file will look something like:

create table posts (
id integer primary key autoincrement,
slug text not null unique,
title text not null,
body text not null,
published integer not null default 0
);

Looks right. Commit it as-is.

Terminal window
npx sqlfu migrate

This applies any pending migrations to your dev database (.sqlfu/app.db) and records each one in sqlfu_migrations. Run this every time you pull new migrations from the repo.

Create sql/queries.sql:

/** @name getPosts */
select id, slug, title, body, published
from posts
where published = 1
order by id desc
limit :limit

Query files live next to the code that calls them. The @name comment is the query’s identity: it shows up in generated types, observability spans, and error messages.

Terminal window
npx sqlfu generate

sqlfu reads your .sql files against definitions.sql by default and emits typed wrappers into sql/.generated/. For the @name getPosts query you get a getPosts function with typed params and a typed result row attached via a namespace (getPosts.Params, getPosts.Result). The function also carries .sql and .query (including name: "getPosts") as static properties used by observability hooks.

Because the default generate.authority is desired_schema, generation does not need a live database. This walkthrough still runs migrate first so the next step can call the wrapper against .sqlfu/app.db.

import {DatabaseSync} from 'node:sqlite';
import {createNodeSqliteClient} from 'sqlfu';
import {getPosts} from './sql/.generated/queries.sql.ts';
const db = new DatabaseSync('./.sqlfu/app.db');
const client = createNodeSqliteClient(db);
const posts = await getPosts(client, {limit: 10});
// ^? Array<{id: number, slug: string, title: string, body: string, published: number}>

Params and result rows are fully typed. Your IDE hover shows the inferred row type directly. The getPosts.query.name field ("getPosts") travels with every call to OpenTelemetry spans, Sentry errors, and Datadog metrics — see Observability.

node:sqlite is built into Node 22+. Using a different runtime or driver? See Adapters for Bun, Turso, D1, Expo, and others — the same generated wrappers work unchanged across all of them.

Edit definitions.sql and add a column:

create table posts (
id integer primary key autoincrement,
slug text not null unique,
title text not null,
body text not null,
excerpt text,
published integer not null default 0
);

Then draft the migration:

Terminal window
npx sqlfu draft

The generated file will contain:

alter table posts add column excerpt text;

That is one line, not a full table rebuild. The diff engine compares the schema you have now (replayed from migration history) against the schema you declared in definitions.sql, and emits only the statements needed to close the gap. Adding a nullable column is a single alter table statement; SQLite supports it directly without recreating the table.

Review the file, commit it, then run npx sqlfu migrate and npx sqlfu generate to update the live schema and regenerate the typed wrapper.

Pick the path that matches where you are:

  • Need Turso, D1, Bun, Durable Objects, or another runtime? Guides has end-to-end setup pages; Adapters has the compact adapter reference.
  • Want to understand how migrations work? SQL migrations: the replay-based model, what sqlfu check verifies, and what to do when a migration fails.
  • Want to handle adapter errors consistently? Errors: normalized SqlfuError.kind values and handler examples.
  • Need validated rows for tRPC or forms? Runtime validation: opt-in validation with arktype, valibot, or zod baked into the generated wrappers.
  • Need multiple queries per file or list params? Type generation from SQL: @name, inferred IN (:ids) lists, object dot paths, and inferred bulk inserts.
  • Need query telemetry? Observability: query names in OpenTelemetry spans, Sentry errors, PostHog events, and Datadog metrics.
  • Want to see more generated type shapes? Generate examples: real query fixtures showing param and result types for common patterns.
  • Want a visual interface for your database? Admin UI: run queries, inspect tables, and draft migrations in the browser.