Skip to content

Bun — bun:sqlite

Bun's built-in bun:sqlite is fast and supports loadable extensions out of the box on Linux and macOS.

Load and set up

ts
import { Database } from "bun:sqlite";

const db = new Database("app.db");
db.loadExtension("./dist/rls.so");

db.run(`
  CREATE TABLE documents(id INTEGER PRIMARY KEY, owner_id INT NOT NULL, title TEXT);
  INSERT INTO documents(owner_id, title) VALUES (1,'Alice'), (2,'Bob');
`);

db.query("SELECT rls_protect(?)").get(
  JSON.stringify({ table: "documents", select: "owner_id = rls_ctx('user_id')" }));
db.query("SELECT rls_seal(?)").get(process.env.RLS_TOKEN!);
db.query("SELECT rls_guard()").get();

Serve queries

ts
db.query("SELECT rls_set_context(?)").get(JSON.stringify({ user_id: 1 }));
db.query("SELECT title FROM documents").all();   // [{ title: "Alice" }]

If loadExtension complains

Older Bun on macOS fell back to Apple's system SQLite, which is compiled without extension support. If loadExtension throws that extensions aren't enabled, point Bun at a full SQLite build once at startup:

ts
import { Database } from "bun:sqlite";
Database.setCustomSQLite("/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib");

On Linux this isn't needed — Bun bundles an extension-capable SQLite.

Notes

  • db.query(...) returns a cached, reusable prepared statement; db.prepare gives a one-off. Either works for the RLS calls.
  • Write results: changes is 0 through a policy view (trigger work isn't counted) — use RETURNING or rls_last_rowid(). See the caveats.
  • The per-query context is connection state; keep the bind + query together, or use a connection per concurrent unit of work.

A loadable SQLite extension. No warranty; test against your own schema.