Database providers (Postgres)
Provider matrix for the Postgres dialect — self-host, Supabase, Neon, Xata, Vercel PG — and why Cloudflare Workers is not on it.
Database providers
Section titled “Database providers”Backlex runs on two SQL dialects — Postgres (pg) and SQLite — and within
each dialect it accepts several wire-compatible providers. Picking a
provider is mostly an env-var change; the schema, migrations, permission
compiler, and queries are the same for every entry in a column.
This page documents the Postgres column. For the SQLite column see sqlite-providers.md (Bun SQLite, Cloudflare D1, Turso/libSQL, LiteFS).
Selection rule (one line)
Section titled “Selection rule (one line)”buildContext picks the database in this order, first match wins:
env.D1binding (Cloudflare Workers) → SQLite via D1env.LIBSQL_URL→ SQLite via libSQL/Turso (fetch-based, edge-safe)env.DATABASE_URL→ Postgres- otherwise → SQLite via
bun:sqliteat./.data/backlex.sqlite
On Cloudflare Workers, step 3 is refused: that bundle ships no Postgres
driver, so a DATABASE_URL with no D1 and no LIBSQL_URL fails fast with a
message naming the alternatives. See Deployment footnote 6.
The Postgres driver is chosen with DATABASE_DRIVER:
postgres-js(default) —postgresovernode:net/node:tls. Works on Bun, Node, CF Workers (undernodejs_compat), Netlify Edge. Setsprepare: falseso Supabase’s transaction pooler / PgBouncer work withoutprepare_statementerrors.neon-http—@neondatabase/serverlessoverfetch(). The only driver that works on Vercel Edge. Picked automatically on stateless edge runtimes if you don’t setDATABASE_DRIVERexplicitly.
Postgres provider matrix
Section titled “Postgres provider matrix”| Provider | DATABASE_URL example | Driver | pgvector | FTS (tsvector) | JSONB | Notes |
|---|---|---|---|---|---|---|
| Self-hosted PG | postgres://user:pass@host:5432/backlex | postgres-js | ✅ (CREATE EXTENSION vector) | ✅ | ✅ | Reference setup. |
| Supabase | postgres://postgres.<ref>:<pwd>@aws-0-<region>.pooler.supabase.com:6543/postgres | postgres-js | ✅ (toggle in Dashboard → Extensions) | ✅ | ✅ | Use transaction pooler (6543), not session pooler. |
| Neon | postgres://<user>:<pwd>@ep-<id>-pooler.<region>.aws.neon.tech/<db>?sslmode=require | postgres-js (Bun/CF/Node) / neon-http (Vercel Edge) | ✅ (CREATE EXTENSION vector) | ✅ | ✅ | Auto-suspend wakes on first query; first request after idle ≈ 100–500 ms. |
| Xata | postgres://<workspace-id>:<api-key>@<workspace>.<region>.xata.sh:5432/<db>:<branch>?sslmode=require | postgres-js | ❌ — pair with Vectorize or another PG | ✅ | ✅ | Branch lives in the URL after the database name (<db>:<branch>). |
| Vercel Postgres | (issued by vercel env) | neon-http | ✅ | ✅ | ✅ | Re-branded Neon under the hood; identical config. |
Not on this table: Cloudflare Workers. Postgres is unavailable there in any form, Hyperdrive included — the Workers bundle aliases
postgres,@neondatabase/serverlessand@backlex/db/pgto shims so the pg schema anddrizzle-orm/pg-corestay out of the cold-start graph. Use D1 or libSQL/Turso on Workers, or deploy the Bun / Node / Vercel / Netlify target for Postgres.
Per-provider gotchas
Section titled “Per-provider gotchas”Supabase
Section titled “Supabase”- The transaction pooler on port
6543is the right URL — it terminates prepared statements before they hit Postgres, which is why Backlex setsprepare: falseon the postgres-js driver. Using the session pooler (5432) also works, but you lose the pooler’s protection against connection exhaustion on serverless invocations. - Enable pgvector once per project: Database → Extensions →
vector→ Enable. - Supabase Realtime is not used. Backlex has its own realtime layer
(
docs/realtime.md) over SSE / Durable Objects, which respects the permission DSL — Supabase Realtime ignores it.
- Default =
postgres-js. Switch toDATABASE_DRIVER=neon-httponly when the runtime can’t opennode:net(Vercel Edge) —neon-httphas higher per-query latency (one HTTP round-trip per statement, no pipelining). - Enable pgvector once:
CREATE EXTENSION IF NOT EXISTS vector;. - Read replicas: set
DATABASE_REPLICA_URLto the replica endpoint; the driver and pgvector requirement match the primary. - The scale-to-zero behaviour means the first request after several minutes of idle waits for the compute to wake. Auto-migrate at boot amortises this on cold start; subsequent requests are normal.
- Xata speaks standard Postgres — the
postgres-jsdriver works out of the box with the connection string from the Xata dashboard. - No pgvector. Backlex detects
.xata.shURLs and routes vector endpoints tonoVectorAdapterunless a Cloudflare Vectorize binding is present (VECTORIZE_OPENAI/VECTORIZE_OPENAI_LARGE/VECTORIZE_BGE_M3/VECTORIZE_SELF_HOST_BGE_M3). Two combinations that work:- Xata + Cloudflare Workers → bind a Vectorize index, vector endpoints route there automatically.
- Xata for relational + another PG for vector → not supported in a single deploy (Backlex uses one DB). Use Vectorize.
- The URL embeds the branch name after the database with a colon:
…/<db>:<branch>. Backlex doesn’t manage Xata branches; the branch is fixed at deploy time via the URL. - Workspace-level full-text search via Xata’s
/searchendpoint is also not used — Backlex uses Postgrestsvector+websearch_to_tsqueryon the underlying table, which goes through the standard wire protocol.
Cloudflare Workers — why Postgres is absent
Section titled “Cloudflare Workers — why Postgres is absent”Hyperdrive would be the natural answer (a pooler + query cache in front of any
provider above, speaking the Postgres wire protocol). It is not supported,
and the reason is the bundle rather than the protocol: @backlex/db/pg is
statically imported across ~80 files, so leaving it un-aliased would pull every
pgTable definition plus drizzle-orm/pg-core into the eager cold-start graph
of a Worker. vite.config.ts therefore aliases it — along with postgres and
@neondatabase/serverless — to shims, which makes a Postgres-capable Workers
build a genuinely different bundle rather than a configuration flag.
Reach for libSQL/Turso (LIBSQL_URL) when you want a networked database on
Workers: it is fetch-based, edge-safe, and not shimmed.
Auto-migrate compatibility
Section titled “Auto-migrate compatibility”Boot-time auto-migrate (packages/db/src/auto-migrate.ts) runs the bundled
SQL migrations on first request. It is idempotent — running it against
a database that already has the schema is a no-op — and failure-tolerant:
a single bad statement is logged and skipped, the rest of the boot continues.
This matters most for managed providers where you can’t run
bun run db:migrate:pg yourself between deploys:
- Supabase / Neon / Vercel PG — auto-migrate runs on first request after the deploy completes. Same code path as self-host.
- Xata — same, except any migration statement that depends on pgvector will be logged-and-skipped; this is intentional.
- CF Workers + D1 — out of scope:
wrangler d1 migrations applyruns in the Workers Build command before the worker boots.
If you want to run migrations explicitly before the first request, point
DATABASE_URL at the target and run bun run db:migrate:pg from your
laptop or CI — same SQL, same idempotency.
Choosing a provider
Section titled “Choosing a provider”- Pure relational + vector + small team → Supabase. Free tier covers development; pgvector is one click; pooler URL just works.
- Edge-native deploy (Vercel Edge / wide region spread) → Neon with
DATABASE_DRIVER=neon-http. - Already on Cloudflare, want SQLite → D1 (see sqlite-providers.md).
- Already on Cloudflare, want a networked DB → libSQL/Turso. Postgres is not available on Workers at all; deploy the Bun / Node / Vercel / Netlify target if you need it.
- Branching-first workflow (preview-per-PR DB) → Neon (branching built-in) or Xata (branches are a URL suffix). Xata branches are free on the dashboard; Neon branches are charged by storage.
See also
Section titled “See also”docs/deployment.md— full deploy steps per runtimedocs/architecture.md— how the dual-dialect schema worksdocs/sqlite-providers.md— Bun SQLite, D1, Turso/libSQL, LiteFSapps/web/.dev.vars.example— copy/paste-ready env blocks