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

sqlfu doesn’t ship its own database driver. It sits on top of whichever database client you already use and gives you the same typed client surface on top.

This page lists the runtime adapters that ship in sqlfu, with a copy-paste snippet for each. The SQLite-compatible adapters have end-to-end guides today. createNodePostgresClient() is runtime-only: it adapts a pg-compatible pool for application queries, while the broader @sqlfu/pg dialect/toolchain docs are still in progress.

For an end-to-end setup, use the matching guide:

RuntimeGuide
Durable ObjectsDurable Objects
Cloudflare D1Cloudflare D1
Node SQLite / better-sqlite3 / native libsqlNode SQLite
Bun SQLiteBun SQLite
Turso and libSQLTurso and libSQL
Expo SQLiteExpo SQLite
sqlite-wasmsqlite-wasm

If you already know which driver you want to use, jump to the section below. If you’re picking from scratch, see Choosing an adapter at the bottom. For the shared client contract, see Runtime client.

Most query libraries force every database call to be async, even when the underlying driver is synchronous. sqlfu preserves the sync or async nature of the driver you brought:

  • Give it better-sqlite3 and you get a SyncClient. client.all(...) returns rows, not a Promise<rows>.
  • Give it @libsql/client and you get an AsyncClient. Same surface, but promise-returning.
  • Generated wrappers and applyMigrations() follow the same split.

That is why the matrix below has a Sync/Async column. Swapping from one sync driver to another is usually a one-line boundary change. Swapping from sync to async is a real application change, and sqlfu leaves that visible in the types.

Generated wrappers are the main application path: put stable queries in .sql files, run sqlfu generate, and call the generated function. client.prepare(sql) is the lower-level client API for SQL that needs to stay dynamic or ad-hoc without reaching through to client.driver.

Use it when you want to reuse one statement handle, bind named parameters directly, or call .all() and .run() against the same SQL string. The handle follows the same sync/async split as the client:

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'});
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'});

Prepared handles expose .all(params), .run(params), and .iterate(params). params can be a positional array ([id]) or a named object ({slug}). Adapters that have native prepared statements hold the driver handle and dispose it when the using scope exits. Adapters whose driver only exposes an exec/execute API provide a compatible shim: the method still exists, but each call re-issues the SQL through the driver.

Named object keys are bare identifiers. SQL written as :slug, @slug, or $slug all bind from {slug: 'hello-world'}; sqlfu adapters translate that shape to the underlying driver, including sqlite-wasm. For positional ? placeholders, pass an array.

Driver packageRuntimeWhere it runsSync/AsyncAdapter factory
better-sqlite3NodeLocal file / in-memorySynccreateBetterSqlite3Client
node:sqliteNode ≥ 22Local file / in-memorySynccreateNodeSqliteClient
bun:sqliteBunLocal file / in-memorySynccreateBunClient
libsqlNodeLocal file / embedded replicaSynccreateLibsqlSyncClient
@libsql/clientNode / Deno / edge runtimesLocal file: or remote libsql://AsynccreateLibsqlClient
@tursodatabase/databaseNodeLocal / embedded (Turso’s next engine)AsynccreateTursoDatabaseClient
@tursodatabase/serverlessAny fetch() runtimeRemote Turso Cloud (HTTP)AsynccreateTursoServerlessClient
@tursodatabase/syncNodeLocal file + sync to Turso CloudAsynccreateTursoDatabaseClient
Cloudflare D1DatabaseCloudflare WorkersD1AsynccreateD1Client
Durable Object SqlStorageCloudflare Durable ObjectsPer-DO embedded SQLiteSynccreateDurableObjectClient
expo-sqliteExpo (React Native)On-device SQLiteAsynccreateExpoSqliteClient
@sqlite.org/sqlite-wasmBrowsersOPFS / in-memoryAsynccreateSqliteWasmClient
pgNodePostgreSQLAsynccreateNodePostgresClient

Every factory takes the underlying driver’s database/connection object as its single argument and returns a sqlfu client. None of these drivers are peer dependencies: install only the one you actually use.

sqlfu/better-auth is a Better Auth adapter wrapper, not a sqlfu database driver adapter. It exists so Better Auth’s auth generate command can write the auth-owned part of sqlfu’s configured definitions.sql, while sqlfu still owns database diffs and migrations.

import {betterAuth} from 'better-auth';
import {sqlfuBetterAuthAdapter} from 'sqlfu/better-auth';
export const auth = betterAuth({
database: sqlfuBetterAuthAdapter(),
});

Then run generation, draft the sqlfu migration, and apply it:

Terminal window
npx auth@latest generate --yes
npx sqlfu draft
npx sqlfu migrate

See Better Auth for first-run append behavior, the managed region format, runtime adapter delegation, and troubleshooting.

import {DatabaseSync} from 'node:sqlite';
import {createNodeSqliteClient} from 'sqlfu';
const db = new DatabaseSync('app.db');
const client = createNodeSqliteClient(db);
import {Database} from 'bun:sqlite';
import {createBunClient} from 'sqlfu';
const db = new Database('app.db');
const client = createBunClient(db);
import Database from 'better-sqlite3';
import {createBetterSqlite3Client} from 'sqlfu';
const db = new Database('app.db');
const client = createBetterSqlite3Client(db);
import Database from 'libsql';
import {createLibsqlSyncClient} from 'sqlfu';
const db = new Database('app.db');
const client = createLibsqlSyncClient(db);

The new Turso-built engine, with native bindings. Same API shape as @tursodatabase/sync.

import {connect} from '@tursodatabase/database';
import {createTursoDatabaseClient} from 'sqlfu';
const db = await connect('app.db'); // or ':memory:'
const client = createTursoDatabaseClient(db);

@libsql/client: Turso Cloud (or local file:)

Section titled “@libsql/client: Turso Cloud (or local file:)”
import {createClient} from '@libsql/client';
import {createLibsqlClient} from 'sqlfu';
const raw = createClient({
url: process.env.TURSO_DATABASE_URL!, // libsql://...
authToken: process.env.TURSO_AUTH_TOKEN,
});
const client = createLibsqlClient(raw);

The same adapter works against a local file when url is file:app.db.

@tursodatabase/serverless: HTTP, no native deps

Section titled “@tursodatabase/serverless: HTTP, no native deps”
import {connect} from '@tursodatabase/serverless';
import {createTursoServerlessClient} from 'sqlfu';
const conn = connect({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN,
});
const client = createTursoServerlessClient(conn);

No native bindings: runs on any runtime with fetch() (Vercel Edge, Cloudflare Workers, Deno Deploy, AWS Lambda).

@tursodatabase/sync: local file, synced to Turso

Section titled “@tursodatabase/sync: local file, synced to Turso”

Same adapter as @tursodatabase/database; the difference is at the driver level (the driver keeps a local file and knows how to push()/pull() to a remote Turso DB).

import {connect} from '@tursodatabase/sync';
import {createTursoDatabaseClient} from 'sqlfu';
const db = await connect({
path: 'local.db',
url: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
});
await db.connect();
const client = createTursoDatabaseClient(db);
// sync at your own cadence; sqlfu doesn't own this
await db.push();
await db.pull();

createNodePostgresClient() adapts a pg pool for application queries. This adapter is runtime-only and lives in the main sqlfu package. The broader Postgres dialect/toolchain work lives in @sqlfu/pg; fuller docs and examples for that package are still in progress.

import {Pool} from 'pg';
import {createNodePostgresClient} from 'sqlfu';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const client = createNodePostgresClient(pool);
import {createD1Client} from 'sqlfu';
export default {
async fetch(_req, env: {DB: D1Database}) {
const client = createD1Client(env.DB);
// ...
},
};
import {DurableObject} from 'cloudflare:workers';
import {createDurableObjectClient} from 'sqlfu';
import {migrate} from '../migrations/.generated/migrations.ts';
export class Counter extends DurableObject {
client: ReturnType<typeof createDurableObjectClient>;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.client = createDurableObjectClient(ctx.storage);
migrate(this.client);
}
}

Pass ctx.storage, not ctx.storage.sql. The SQL handle is enough for queries, but the full storage object gives sqlfu access to Cloudflare’s transactionSync() API, so each migration is applied inside a real Durable Object storage transaction. If you need a query-only escape hatch, pass {sql: ctx.storage.sql} explicitly.

The generated migration module is emitted by sqlfu generate when migrations is configured. Commit migrations/*.sql; import migrate from migrations/.generated/migrations.ts into the Worker bundle; let every Durable Object instance call it during startup. migrate() is idempotent: once a given Durable Object’s private SQLite database has a row in sqlfu_migrations, that migration is skipped on later starts.

import * as SQLite from 'expo-sqlite';
import {createExpoSqliteClient} from 'sqlfu';
const db = await SQLite.openDatabaseAsync('app.db');
const client = createExpoSqliteClient(db);
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
import {createSqliteWasmClient} from 'sqlfu';
const sqlite3 = await sqlite3InitModule();
const db = new sqlite3.oo1.DB('file:app.db?vfs=opfs');
const client = createSqliteWasmClient(db);

A few rules of thumb:

  • You want fast local dev and then “a real database” in prod: use @libsql/client for both. Set url: 'file:app.db' locally and libsql://... in prod. No code changes needed at the sqlfu layer.
  • You need zero native deps (edge workers, serverless, Lambda): @tursodatabase/serverless or Cloudflare D1.
  • You want an embedded database that can sync to the cloud: @tursodatabase/sync.
  • You want the fastest pure-local experience on Node: better-sqlite3 or @tursodatabase/database.
  • You’re on Bun: bun:sqlite.
  • You’re on Node ≥ 22 and want no extra deps: node:sqlite.
  • Mobile / browser: Expo SQLite or sqlite-wasm.

And probably the best thing: you don’t have to choose one. You can define separate entrypoints for your local, test and production environments and write your application logic using the shared sqlfu Client interface, then pass around two different clients, and they’ll behave in the same way.

Each adapter is a thin function that wraps a driver into a SyncClient or AsyncClient. The existing adapters in src/adapters/ are the reference. Shape:

  • all(query) → rows
  • run(query){rowsAffected?, lastInsertRowid?}
  • raw(sql) → multi-statement string execution
  • iterate(query) → row iterator
  • prepare(sql) → reusable statement handle with .all, .run, .iterate, and a dispose method
  • transaction(fn) → run fn inside a transaction (the sqlfu/core/sqlite helpers provide surroundWithBeginCommitRollback{Sync,Async} that implement this for you using begin/commit/rollback)

If your driver has a native prepared statement, wrap it. If it does not, implement prepare(sql) as a small shim that captures the SQL string and calls the driver’s normal execution method on each .all, .run, or .iterate. The method should still return a disposable handle so callers can use using / await using uniformly.

If your driver is SQLite-compatible but not listed, opening a PR with a new adapter file + a test file in test/adapters/ is usually a small change.