Pick a path

There are two ways to talk to the engine. They share the same SQL and the same connection-string model, so you can start embedded and add a server later without rewriting queries.

PathHow it runsReach for it when
Embedded (@twilldb/bun)The engine links in-process via bun:ffi; queries are function calls, no network.You run on Bun and want the lowest latency and zero infrastructure — a single app, a CLI, an edge/serverless function.
Server (Postgres wire)The same engine behind a Postgres-wire listener; any Postgres driver/ORM connects.You are not on Bun, want to use an existing Postgres ORM, or need many processes to share one database.

This page covers the embedded path. For the server path, start at Postgres client and the per-language guides under Connect as server.

A · New project from scratch

Scaffold a Bun project and add the package. The prebuilt engine arrives with it — no Rust, no build step.

mkdir my-app && cd my-app
bun init -y
bun add @twilldb/bun

Create a single module that owns the database handle so the rest of your code imports one ready-to-use instance:

// src/db.ts
import { open } from "@twilldb/bun";

// One connection string drives the backend. Default to a local file;
// override with TWILLDB_URL (e.g. s3://my-bucket/app) in production.
export const db = open(process.env.TWILLDB_URL ?? "file://./data/app.db");

// Create tables once, on startup. DDL runs in autocommit only.
db.exec(`
  CREATE TABLE IF NOT EXISTS notes (
    id    INTEGER PRIMARY KEY,
    body  TEXT NOT NULL,
    done  INTEGER NOT NULL DEFAULT 0
  )
`);
// src/index.ts
import { db } from "./db";

// Parameterized writes go through query() (exec takes no params); it returns
// the (empty) result set for an INSERT.
db.query("INSERT INTO notes (id, body) VALUES (?, ?)", [1, "first note"]);

const rows = db.query("SELECT id, body, done FROM notes WHERE done = ?", [0]);
console.log(rows);   // values come back as strings: [{ id: "1", body: "first note", done: "0" }]
bun run src/index.ts

Keep the data file out of git

The file:// backend writes a database file (here ./data/app.db) plus its WAL. Add the data directory to .gitignore — e.g. echo "data/" >> .gitignore — so local data never gets committed.

B · Add to an existing app

If you already have a Bun project, you only need the package and a db module. Nothing else in your build changes.

bun add @twilldb/bun
echo "data/" >> .gitignore

Drop in the same src/db.ts module shown above and import db wherever you query. A common shape — wiring it into an HTTP handler:

import { db } from "./db";

Bun.serve({
  port: 3000,
  routes: {
    "/notes": {
      GET: () => Response.json(db.query("SELECT id, body, done FROM notes")),
      POST: async (req) => {
        const { id, body } = await req.json();
        db.query("INSERT INTO notes (id, body) VALUES (?, ?)", [id, body]);
        return new Response(null, { status: 201 });
      },
    },
  },
});

Framework-specific wiring (Hono, Elysia, Next.js, plain Bun HTTP) is covered under Connect as embedded — see the overview.

One writer per database

The engine is single-writer per database. Within one Bun process, multiple imports of src/db.ts share the same handle — that is the intended pattern. Do not open the same file:// path from several independent OS processes at once; if you need multi-process access, run the server instead. See Connection pooling for the details.

Patterns that keep it clean

  • SHOULD Own the handle in one module (the db.ts singleton above). Importing it elsewhere reuses the same in-process database — don't call open() per request.
  • SHOULD Use ? placeholders with a value array for anything user-supplied — values never touch the SQL text. Note exec(sql) takes no parameters, so parameterized statements (including writes) go through query(sql, params) or a prepared statement. Every column comes back as a string (SQL NULLnull); cast to numbers/dates yourself.
  • SHOULD Wrap multi-statement writes in db.transaction(fn) — it commits on return (blocking until the WAL is durable) and rolls back if fn throws.
  • MAY Move the connection string to TWILLDB_URL so the same code runs on file:// locally and s3:///r2:///gs:// in production with no edit. Credentials come from the environment, never code.
  • MAY Use using for short-lived handles (scripts, tests) so they dispose at scope exit; a long-lived server handle can live for the process lifetime.
// transaction: all-or-nothing, durable on return
db.transaction((tx) => {
  tx.query("UPDATE notes SET done = 1 WHERE id = ?", [1]);
  tx.query("INSERT INTO notes (id, body) VALUES (?, ?)", [2, "follow-up"]);
});   // ← COMMIT returns only after the WAL is fsync'd

With an AI coding agent

Using Claude Code, Cursor, or a similar agent? Paste one of the blocks below into your agent. They carry the few facts an agent needs to get the integration right the first time — the npm package name, the Bun requirement, the connection-string model, and the engine's actual behaviour (strings out, autocommit DDL, single writer).

New project

Set up Twill DB as the database for a new Bun project.

Requirements & facts:
- Runtime is Bun (>= 1.1). The package is `@twilldb/bun` on npm; install with
  `bun add @twilldb/bun`. A prebuilt native engine ships as optional deps — no
  Rust, no build step, no postinstall.
- Open a database with `open(url)` from "@twilldb/bun". Use a connection string
  from `process.env.TWILLDB_URL`, defaulting to "file://./data/app.db".
- Create ONE module `src/db.ts` that calls `open(...)` once and exports the
  handle; everything else imports it. Do not call open() per request.
- API: `db.exec(sql)` for statements with NO parameters (DDL, simple writes;
  returns rows affected). `db.query(sql, params?)` for reads AND for any
  parameterized statement — exec() does not take params, so writes with `?`
  placeholders go through query() (or a prepare()d statement). `db.transaction(fn)`
  wraps multi-statement writes.
- Behaviour to respect: every value comes back as a STRING (SQL NULL -> null),
  so cast numbers/dates yourself. DDL (CREATE/DROP TABLE) runs in autocommit
  ONLY — never inside transaction(). Always use `?` placeholders with a value
  array (via query/prepare) for user input.
- Add `data/` to .gitignore (the file:// backend writes a db file + WAL there).

Deliver: package install, `src/db.ts` with schema bootstrap, and a small
`src/index.ts` that inserts and queries a row to prove it works.

Existing project

Integrate Twill DB into this existing Bun project as its database.

Requirements & facts:
- Install `@twilldb/bun` with `bun add @twilldb/bun` (prebuilt engine ships with
  it — no Rust/build step). Runtime must be Bun >= 1.1.
- Add a single `db` module: call `open(process.env.TWILLDB_URL ?? "file://./data/app.db")`
  once and export the handle. Reuse it everywhere; never open() per request.
- Find where the app currently reads/writes data and route it through:
  `db.exec(sql)` (no-parameter statements/DDL), `db.query(sql, params?)` (reads
  AND any parameterized statement — exec() takes no params), and
  `db.transaction(fn)` (multi-statement writes, durable on return).
- Respect engine behaviour: values come back as STRINGS (NULL -> null); DDL is
  autocommit-only (keep CREATE/DROP TABLE out of transaction()); always bind
  user input with `?` placeholders + a value array.
- The engine is single-writer per database within one process — the shared
  module handle is correct; do not open the same file:// path from multiple
  processes. Add `data/` to .gitignore.

Deliver: the db module, the migrated data access, and a note on what changed.

Point your agent at the source of truth

For deeper tasks, also give your agent these pages: Embedded (bun:ffi) (full API), SQL reference (the supported SQL surface — it is a focused subset), and Connect to your database (backends). That keeps the agent from assuming full Postgres SQL.

Next steps

Twill DB documentation · Licensed under AGPL-3.0. · Author