Database providers (SQLite)
Provider matrix for the SQLite dialect — Bun SQLite, Cloudflare D1, Turso / libSQL, and LiteFS.
Database providers (SQLite)
backlex’ SQLite dialect ships against four transports — local Bun, D1, libSQL/Turso, and LiteFS. The schema, migrations, permission compiler, and queries are identical across all of them; what changes is where the bytes live and how the runtime talks to them.
For the Postgres-side matrix (Supabase / Neon / Xata / self-host) see database-providers.md.
Selection rule
buildContext picks the SQLite transport in this order, first match wins:
env.D1binding → Cloudflare D1 (createD1Client)env.LIBSQL_URL→ libSQL / Turso (createLibsqlClient)env.DATABASE_URLset → falls back to Postgres, not SQLite- otherwise → Bun SQLite at
env.SQLITE_PATH(default./.data/backlex.sqlite)
Step 3 means a Postgres URL always wins over the Bun-SQLite fallback. Set
LIBSQL_URL (with no DATABASE_URL) to flip a fresh deploy onto Turso.
SQLite provider matrix
| Provider | Transport | Runtime support | Vector | FTS (fts5) | Migrations applied by |
|---|---|---|---|---|---|
| Bun SQLite | local file | Bun + Node self-host | ❌ — pair with Vectorize / pgvector | ✅ | bun run db:migrate:sqlite (CLI) |
| Cloudflare D1 | binding | CF Workers only | ❌ — pair with Vectorize | ✅ | wrangler d1 migrations apply inside the Workers Build command |
| Turso / libSQL | HTTP / WS / WS-libsql | every runtime (fetch-based) | ❌ — pair with Vectorize | ✅ | bun run db:migrate:libsql (CLI) or boot-time auto-migrate |
| LiteFS (Fly) | local file via FUSE | Bun / Node only (no edge) | ❌ — pair with Vectorize / pgvector | ✅ | Same as Bun SQLite — runs on primary, replicates to read-replicas |
Vector: SQLite has no first-class vector type that backlex uses. Vector
endpoints require either a Cloudflare Vectorize binding (VECTORIZE_OPENAI
/ VECTORIZE_BGE_M3 / …) or switching to a Postgres deploy with
pgvector. Without one, vector endpoints fail loud with a clear message.
Per-provider gotchas
Bun SQLite
- Default for
bun run dev. File path is./.data/backlex.sqlite; the directory is created on first write. PRAGMA journal_mode = WALis set at client construction → concurrent readers + a single writer perform well on a single-process deployment.- Not suitable for multi-process / multi-container deployments — for that use D1, Turso, or LiteFS.
Cloudflare D1
- D1 ships read replicas via the Sessions API. The HTTP layer reads the
x-d1-bookmarkrequest header, pins the session forward, and writes the latest bookmark back on the response so the next request follows the same replica. This is wired in bycreateD1SessionClient; routes don’t need to know it’s happening. - Migrations are applied by the Workers Build command
(
bun run db:migrate:d1:remote && bun run build), not by the runtime auto-migrate path. This keeps cold-start times tight on CF Workers. - Multi-statement HTTP calls aren’t pipelined the way
postgres-jspipelines them — the auto-migrate splitter (-->statement-breakpoint) is what keeps each statement individually addressable.
Turso / libSQL
-
Connection string scheme picks the transport:
libsql://…— Turso’s negotiated WS/HTTPS (recommended)wss://…/ws://…— Hrana WebSocket directlyhttps://…/http://…— Hrana HTTP (one round-trip per statement)file:…— local libSQL file (rare; prefer Bun SQLite)
-
LIBSQL_AUTH_TOKENis required for any Turso URL; optional for self-hostedsqldwith auth disabled. -
The libSQL client is pure JS + fetch — works on every runtime backlex supports including Vercel Edge and Netlify Edge (whereas Bun SQLite needs
bun:sqlite, which those edges don’t have). -
Boot-time auto-migrate runs on first request, same as Postgres. To run migrations explicitly (e.g. from CI before the first request):
Terminal window LIBSQL_URL=libsql://my-db-org.turso.io \LIBSQL_AUTH_TOKEN=eyJ... \bun run --cwd packages/db migrate:libsql -
Embedded replicas (
@libsql/clientsyncUrl) are out of scope for backlex — open a custom client outside the standard adapter if you need them.
LiteFS (Fly.io)
-
Backlex doesn’t talk to LiteFS directly. Point
SQLITE_PATHat the LiteFS-mounted file (e.g./litefs/backlex.sqlite); the Bun SQLite driver opens it the same way it opens any local file. -
Writes only on the primary node. LiteFS replicates one-way from the primary to read-replicas. On Fly, set
primary_regioninfly.tomland usefly-replayto forward write requests:fly.toml [[services.http_checks]]# …backlex checks…[services]# Forward writes (POST/PUT/PATCH/DELETE) to the primary[[services.http_response_headers]]x-fly-replay = "region={{ env.PRIMARY_REGION }}"This sits outside backlex; the app itself doesn’t need a primary/replica concept.
-
LiteFS isn’t an edge transport — it needs FUSE, so it only runs on Bun / Node containers (Fly Machines, Railway, a VPS).
-
PRAGMA journal_modeon LiteFS-mounted files is fixed to a LiteFS-compatible mode by the FUSE layer; thejournal_mode = WALbacklex tries to set may no-op silently — that’s expected.
Auto-migrate compatibility
auto-migrate.ts runs the bundled SQL migrations on first request on every
SQLite transport except D1 (D1 has its own CLI-driven path that fires
during the Workers Build):
| Provider | Auto-migrate on first request? |
|---|---|
| Bun SQLite | ✅ |
| Turso / libSQL | ✅ |
| LiteFS (primary) | ✅ |
| LiteFS (replica) | ✅ (no-op — read-only FS) |
| Cloudflare D1 | ❌ — wrangler d1 migrations apply instead |
The runner is idempotent. A statement that fails (already-applied SQL,
unsupported pragma) is logged and skipped, the rest of the boot
continues. See packages/db/src/auto-migrate.ts for the contract.
Choosing a transport
- Local dev / single-machine deploy → Bun SQLite. Zero config.
- Cloudflare Workers → D1. First-class binding, region-aware replicas, no separate connection string to manage.
- **Multi-region edge (Vercel/Netlify Edge or Workers across providers)
- want SQLite semantics** → Turso. The fetch-based driver works on
every runtime, including the V8-isolate edges that block
bun:sqlite.
- want SQLite semantics** → Turso. The fetch-based driver works on
every runtime, including the V8-isolate edges that block
- Multi-node single-region deploy with low-latency local reads → LiteFS. Best when you already run on Fly.io and need write-locality to one region with replicated reads everywhere else.
See also
docs/database-providers.md— Postgres-side matrix (Supabase, Neon, Xata, …)docs/deployment.md— full deploy steps per runtimedocs/architecture.md— how the dual-dialect schema worksapps/web/.dev.vars.example— copy/paste-ready env blocks