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.

The Client type is the only runtime-specific boundary your application code needs to know about. Everything else — generated query wrappers, migrations, the observability layer — works against this same surface, regardless of which SQLite driver sits beneath it.

The Client type

type Client = SyncClient | AsyncClient;
Both variants share the same method names. The difference is in return types: a SyncClient returns values directly, while an AsyncClient returns promises.
import type {Client} from 'sqlfu';
import {getPosts} from './sql/.generated/queries.sql.ts';

export async function renderFeed(client: Client) {
  const posts = await getPosts(client, {limit: 10});
  return posts.map((post) => `<article>${post.title}</article>`).join('');
}
The only place your code touches a specific driver is where you create the client:
import {DatabaseSync} from 'node:sqlite';
import {createNodeSqliteClient} from 'sqlfu';
import {renderFeed} from './src/app';

const db = createNodeSqliteClient(new DatabaseSync('app.db'));
const html = await renderFeed(db);
Swap the adapter factory and everything else stays unchanged.

The client surface

Both SyncClient and AsyncClient expose the same methods:
MethodPurpose
client.all(query)Execute a row-returning query
client.run(query)Execute a write or DDL statement
client.iterate(query)Stream rows one at a time
client.prepare(sql)Create a reusable statement handle
client.transaction(fn)Run a function inside a driver-backed transaction
client.sqlTagged template helper for inline SQL fragments
client.driverEscape hatch to the underlying driver object
Generated query wrappers accept Client directly. They are the primary data-access layer — client.all, client.run, and client.prepare are there for dynamic or ad-hoc SQL that doesn’t fit into a .sql file.

Sync stays sync

Most query libraries coerce every database call to async, even when the underlying driver is synchronous. sqlfu preserves what you brought. Sync drivers (SyncClient):
  • better-sqlite3
  • node:sqlite
  • bun:sqlite
  • Cloudflare Durable Object storage (SqlStorage)
Async drivers (AsyncClient):
  • @libsql/client
  • @tursodatabase/database / @tursodatabase/sync / @tursodatabase/serverless
  • Cloudflare D1
  • expo-sqlite
  • @sqlite.org/sqlite-wasm
The sync/async nature of the client is visible in the TypeScript type. Passing a SyncClient where an AsyncClient is expected is a type error, not a silent behaviour change. If you swap from one sync driver to another, it is a one-line boundary change. Swapping from sync to async is a real application change — sqlfu keeps that visible.
Set generate.sync: true in sqlfu.config.ts to emit wrappers that explicitly accept a SyncClient and return values directly (no Promise<...> return types). Useful when your project always runs against a synchronous driver such as node:sqlite, better-sqlite3, or bun:sqlite.

Prepared statements

Generated wrappers from .sql files are the primary path. client.prepare(sql) is the lower-level API for SQL that needs to stay dynamic without reaching through to client.driver. Use prepare when you want to reuse one statement handle, bind named parameters directly, or call .all() and .run() against the same SQL string.

Sync prepared statements

sync usage
interface PostRow {
  id: number;
  title: string;
}

using stmt = syncClient.prepare<PostRow>(`
  select id, title
  from posts
  where slug = :slug
`);

const rows = stmt.all({slug: 'hello-world'});
The using declaration releases the underlying driver statement when the scope exits.

Async prepared statements

async usage
interface PostRow {
  id: number;
  title: string;
}

await using stmt = asyncClient.prepare<PostRow>(`
  select id, title
  from posts
  where slug = :slug
`);

const rows = await stmt.all({slug: 'hello-world'});
await using works the same way: the handle’s [Symbol.asyncDispose] method is called when the scope exits.

What prepared handles expose

Each handle exposes .all(params), .run(params), and .iterate(params). Parameters can be positional ([id]) or named ({slug}). Adapters that have native prepared statements hold the driver handle for the lifetime of the using scope. Adapters whose driver exposes only an exec/execute API provide a shim that re-issues the SQL on each call — the interface is identical either way.
params keys for named parameters use the bare name without the leading colon. :slug in SQL matches {slug: 'hello-world'} in the params object, not {':slug': 'hello-world'}.

Where to go next

Adapters

Every built-in adapter factory, with copy-paste setup snippets for each driver.

Type generation

How .sql files become typed generated wrappers via sqlfu generate.

Observability

Wrap a client with tracing, metrics, and error hooks.

Errors

The normalized SqlfuError kinds raised by every adapter.

Build docs developers (and LLMs) love