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.

sqlfu doesn’t ship its own database driver. It wraps whichever SQLite-compatible client you already use and gives you the same typed Client surface on top. The adapter is the only runtime-specific line in your codebase — everything else works against the shared interface.

The adapter pattern

Each adapter factory takes the underlying driver’s database object as its sole argument and returns a sqlfu Client. None of the drivers are peer dependencies of sqlfu: install only the one you use.
import {DatabaseSync} from 'node:sqlite';
import {createNodeSqliteClient} from 'sqlfu';

// Pass the driver object in; get a sqlfu Client out.
const client = createNodeSqliteClient(new DatabaseSync('app.db'));
Swapping adapters is a boundary change — one line at the point where you construct the client. The rest of your application, including all generated query wrappers, keeps working unchanged.

Sync vs. async

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:
  • Pass better-sqlite3 and you get a SyncClient. client.all(...) returns rows, not Promise<rows>.
  • Pass @libsql/client and you get an AsyncClient. Same surface, but promise-returning.
  • Generated wrappers and applyMigrations() follow the same split.
Swapping from one sync driver to another is a one-line boundary change. Swapping from sync to async is a real application change, and sqlfu leaves that visible in the types.

Compatibility matrix

Driver packageRuntimeSync/AsyncFactory function
node:sqliteNode ≥ 22SynccreateNodeSqliteClient
better-sqlite3NodeSynccreateBetterSqlite3Client
bun:sqliteBunSynccreateBunClient
libsqlNodeSynccreateLibsqlSyncClient
@libsql/clientNode / Deno / edgeAsynccreateLibsqlClient
@tursodatabase/databaseNodeAsynccreateTursoDatabaseClient
@tursodatabase/syncNodeAsynccreateTursoDatabaseClient
@tursodatabase/serverlessAny fetch() runtimeAsynccreateTursoServerlessClient
D1Database (Cloudflare)Cloudflare WorkersAsynccreateD1Client
SqlStorage (Durable Objects)Cloudflare Durable ObjectsSynccreateDurableObjectClient
expo-sqliteExpo / React NativeAsynccreateExpoSqliteClient
@sqlite.org/sqlite-wasmBrowsersAsynccreateSqliteWasmClient

Local and embedded adapters

node:sqlite

The built-in SQLite module available in Node ≥ 22. No extra packages needed.
import {DatabaseSync} from 'node:sqlite';
import {createNodeSqliteClient} from 'sqlfu';

const db = new DatabaseSync('app.db');
const client = createNodeSqliteClient(db);

better-sqlite3

The most widely used synchronous SQLite binding for Node. Well-tested and battle-hardened.
import Database from 'better-sqlite3';
import {createBetterSqlite3Client} from 'sqlfu';

const db = new Database('app.db');
const client = createBetterSqlite3Client(db);

bun:sqlite

The built-in SQLite module for Bun. No install needed when running on Bun.
import {Database} from 'bun:sqlite';
import {createBunClient} from 'sqlfu';

const db = new Database('app.db');
const client = createBunClient(db);

libsql (native embedded)

The native libsql binding for Node. Runs locally as a file database, optionally with embedded replica support.
import Database from 'libsql';
import {createLibsqlSyncClient} from 'sqlfu';

const db = new Database('app.db');
const client = createLibsqlSyncClient(db);

@tursodatabase/database

Turso’s next-generation engine with native Node bindings. Uses the same factory as @tursodatabase/sync.
import {connect} from '@tursodatabase/database';
import {createTursoDatabaseClient} from 'sqlfu';

const db = await connect('app.db'); // or ':memory:'
const client = createTursoDatabaseClient(db);

Remote and cloud adapters

@libsql/client — Turso Cloud or local file:

Connects to Turso Cloud over the libsql:// protocol, or to a local file when url is file:app.db. The same adapter works for both — no code changes needed between environments.
import {createClient} from '@libsql/client';
import {createLibsqlClient} from 'sqlfu';

const raw = createClient({
  url: process.env.TURSO_DATABASE_URL!, // libsql://... or file:app.db
  authToken: process.env.TURSO_AUTH_TOKEN,
});
const client = createLibsqlClient(raw);

@tursodatabase/serverless — HTTP, no native dependencies

Connects to Turso Cloud over HTTP using only fetch(). Runs on Vercel Edge, Cloudflare Workers, Deno Deploy, and AWS Lambda without native bindings.
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);

@tursodatabase/sync — local file synced to Turso Cloud

Same createTursoDatabaseClient factory as @tursodatabase/database. The difference is at the driver level: the driver keeps a local SQLite file and knows how to push()/pull() to a remote Turso database.
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();

Cloudflare D1

D1 bindings are injected via the Worker’s env object. Wrap them with createD1Client inside your fetch handler.
import {createD1Client} from 'sqlfu';

export default {
  async fetch(_req: Request, env: {DB: D1Database}) {
    const client = createD1Client(env.DB);
    // use client...
  },
};

Cloudflare Durable Objects

Pass ctx.storage, not ctx.storage.sql. The full storage object gives sqlfu access to Cloudflare’s transactionSync() API so each migration runs inside a real Durable Object storage transaction.
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);
  }
}
migrate() is idempotent. Once a Durable Object’s private SQLite database has a row in sqlfu_migrations for a given migration, that migration is skipped on later starts. Each Durable Object instance manages its own independent database.

Mobile and browser adapters

Expo SQLite

Works with expo-sqlite’s async API on iOS and Android.
import * as SQLite from 'expo-sqlite';
import {createExpoSqliteClient} from 'sqlfu';

const db = await SQLite.openDatabaseAsync('app.db');
const client = createExpoSqliteClient(db);

@sqlite.org/sqlite-wasm (browsers)

Runs SQLite compiled to WebAssembly. Supports OPFS for persistent storage or in-memory databases.
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);

Prepared statements

Generated wrappers from .sql files are the main application path. client.prepare(sql) is the lower-level API for SQL that needs to stay dynamic without reaching through to client.driver. See the client reference for full usage details.
sync
using stmt = syncClient.prepare<PostRow>(`
  select id, title from posts where slug = :slug
`);
const rows = stmt.all({slug: 'hello-world'});
async
await using stmt = asyncClient.prepare<PostRow>(`
  select id, title from posts where slug = :slug
`);
const rows = await stmt.all({slug: 'hello-world'});

Choosing an adapter

Use @libsql/client for both. Set url: 'file:app.db' locally and url: process.env.TURSO_DATABASE_URL in production. No code changes needed at the sqlfu layer.
Use @tursodatabase/serverless (Turso Cloud over HTTP) or Cloudflare D1. Both work on any runtime with fetch().
Use @tursodatabase/sync. The local file works offline; you call push()/pull() at your own cadence.
Use better-sqlite3 or @tursodatabase/database. Both are synchronous with native bindings.
Use bun:sqlite with createBunClient. No extra packages needed.
Use node:sqlite with createNodeSqliteClient. It ships with Node.
Use expo-sqlite for React Native / Expo, or @sqlite.org/sqlite-wasm for browsers.
You don’t have to choose just one. Define separate entrypoints for local, test, and production environments. Your application logic uses the shared Client interface, so you can pass two different clients around and they behave identically.

Writing a custom adapter

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

Implement the core methods

Your adapter object must provide:
  • all(query) — returns rows
  • run(query) — returns {rowsAffected?, lastInsertRowid?}
  • raw(sql) — executes a multi-statement string
  • iterate(query) — returns a row iterator
2

Implement prepare

Return a disposable handle with .all(params), .run(params), .iterate(params), and a dispose method ([Symbol.dispose] for sync, [Symbol.asyncDispose] for async).If the driver has native prepared statements, wrap them. If not, implement prepare as a shim that captures the SQL string and re-issues it through the driver on each call. The using / await using contract still works either way.
3

Implement transaction

The sqlfu/core/sqlite helpers provide surroundWithBeginCommitRollbackSync and surroundWithBeginCommitRollbackAsync. These implement begin / commit / rollback wrapping for you — use them rather than rolling your own.
4

Contribute it upstream

If your driver is SQLite-compatible and not listed above, opening a pull request with a new adapter file and a test file in test/adapters/ is typically a small change.

Build docs developers (and LLMs) love