Skip to content
Data

Database providers (Postgres)

Provider matrix for the Postgres dialect — self-host, Supabase, Neon, Xata, Vercel PG — and why Cloudflare Workers is not on it.

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).

buildContext picks the database in this order, first match wins:

  1. env.D1 binding (Cloudflare Workers) → SQLite via D1
  2. env.LIBSQL_URL → SQLite via libSQL/Turso (fetch-based, edge-safe)
  3. env.DATABASE_URL → Postgres
  4. otherwise → SQLite via bun:sqlite at ./.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) — postgres over node:net / node:tls. Works on Bun, Node, CF Workers (under nodejs_compat), Netlify Edge. Sets prepare: false so Supabase’s transaction pooler / PgBouncer work without prepare_statement errors.
  • neon-http@neondatabase/serverless over fetch(). The only driver that works on Vercel Edge. Picked automatically on stateless edge runtimes if you don’t set DATABASE_DRIVER explicitly.
ProviderDATABASE_URL exampleDriverpgvectorFTS (tsvector)JSONBNotes
Self-hosted PGpostgres://user:pass@host:5432/backlexpostgres-js✅ (CREATE EXTENSION vector)Reference setup.
Supabasepostgres://postgres.<ref>:<pwd>@aws-0-<region>.pooler.supabase.com:6543/postgrespostgres-js✅ (toggle in Dashboard → Extensions)Use transaction pooler (6543), not session pooler.
Neonpostgres://<user>:<pwd>@ep-<id>-pooler.<region>.aws.neon.tech/<db>?sslmode=requirepostgres-js (Bun/CF/Node) / neon-http (Vercel Edge)✅ (CREATE EXTENSION vector)Auto-suspend wakes on first query; first request after idle ≈ 100–500 ms.
Xatapostgres://<workspace-id>:<api-key>@<workspace>.<region>.xata.sh:5432/<db>:<branch>?sslmode=requirepostgres-js❌ — pair with Vectorize or another PGBranch lives in the URL after the database name (<db>:<branch>).
Vercel Postgres(issued by vercel env)neon-httpRe-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/serverless and @backlex/db/pg to shims so the pg schema and drizzle-orm/pg-core stay out of the cold-start graph. Use D1 or libSQL/Turso on Workers, or deploy the Bun / Node / Vercel / Netlify target for Postgres.

  • The transaction pooler on port 6543 is the right URL — it terminates prepared statements before they hit Postgres, which is why Backlex sets prepare: false on 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 to DATABASE_DRIVER=neon-http only when the runtime can’t open node:net (Vercel Edge) — neon-http has 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_URL to 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-js driver works out of the box with the connection string from the Xata dashboard.
  • No pgvector. Backlex detects .xata.sh URLs and routes vector endpoints to noVectorAdapter unless 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 /search endpoint is also not used — Backlex uses Postgres tsvector + websearch_to_tsquery on 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.

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 apply runs 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.

  • 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.
  • docs/deployment.md — full deploy steps per runtime
  • docs/architecture.md — how the dual-dialect schema works
  • docs/sqlite-providers.md — Bun SQLite, D1, Turso/libSQL, LiteFS
  • apps/web/.dev.vars.example — copy/paste-ready env blocks