Runtime client
Most application code should only know about the sqlfu Client interface and
the generated query functions you import from .generated/.
The client is deliberately small: it adapts the SQLite driver you already use, then exposes the same SQL-first surface everywhere.
That matters for production risk. sqlfu is still pre-alpha, but the runtime
client is an extremely thin wrapper around mature drivers such as node:sqlite,
better-sqlite3, bun:sqlite, libsql, D1, and Durable Object storage.
Generated wrappers are plain functions that build SQL plus args and call the
client. The rougher surfaces are more likely to be workflow tools such as
drafting migrations, generation, linting, formatting, and the Admin UI. Those
usually run before deployment rather than in your hot application path.
import type {Client} from 'sqlfu';import {getPosts} from './sql/.generated/get-posts.sql';
export async function renderFeed(client: Client) { const posts = await getPosts(client, {limit: 10}); return posts.map((post) => `<article>${post.title}</article>`).join('');}The boundary where you create the client is the only runtime-specific part:
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 the rest of the app can keep using the same generated functions.
The surface
Section titled “The surface”The shared client shape is:
type Client = SyncClient | AsyncClient;Both variants expose:
client.all(query)for row-returning SQLclient.run(query)for writes and DDLclient.iterate(query)for streaming rowsclient.prepare(sql)for reusable ad hoc statementsclient.transaction(fn)for driver-backed transactionsclient.sqlfor small inline SQL fragmentsclient.driverwhen you need to escape to the underlying database driver
Generated query wrappers accept this same Client shape, so your authored SQL
files become the stable data-access layer rather than a second runtime API to
learn.
Sync stays sync
Section titled “Sync stays sync”sqlfu preserves the sync or async nature of the driver you brought.
If you use better-sqlite3, node:sqlite, bun:sqlite, or Durable Object
storage, a generated query can return rows directly. If you use @libsql/client,
Cloudflare D1, Expo SQLite, or sqlite-wasm, the same wrapper returns a promise.
That distinction is visible in the TypeScript type. sqlfu does not turn a synchronous driver into an async one, and it does not pretend an async driver can run synchronously.
Where to go next
Section titled “Where to go next”- Adapters lists every built-in client factory.
- Type generation from SQL explains how
.sqlfiles become generated wrappers. - Observability shows how to wrap a client with tracing, metrics, and error hooks.
- Errors lists the normalized
SqlfuErrorkinds raised by adapters.