Postgres providers
Any modern Postgres works. The only footgun is transaction poolers — reach for DIRECT_DATABASE_URL when you hit one.
Orbit's only hard database requirement is PostgreSQL 14+ (the kit uses WITH (auto_vacuum = ...)-style features and Postgres-specific JSON operators). It does not ship its own database — docker-compose.yml leaves DATABASE_URL external on purpose. Pick a provider, point at it.
The env vars
| Var | Who reads it | When to set it |
|---|---|---|
DATABASE_URL | API runtime, migration tooling at build time, graphile-worker by default | Always. |
DIRECT_DATABASE_URL | Migration CLI (Prisma or drizzle-kit) | Only when DATABASE_URL is a transaction pooler. Skip if you're hitting Postgres directly. |
WORKER_DATABASE_URL | graphile-worker (jobs) | Only when using graphile with a pooler upstream — workers need LISTEN/NOTIFY which poolers drop. |
If you're not using graphile-worker (i.e. JOBS_PROVIDER=qstash or unset), you can ignore WORKER_DATABASE_URL entirely.
Provider recipes
Neon
Serverless Postgres. The pooled and direct URLs are separate endpoints on the same database:
apps/api/.env (prod)
# Pooled connection (PgBouncer in transaction mode, port 5432)DATABASE_URL="postgresql://user:pass@ep-xxx-pooler.region.aws.neon.tech/orbit?sslmode=require"
# Direct connection for migrations + graphile LISTEN/NOTIFYDIRECT_DATABASE_URL="postgresql://user:pass@ep-xxx.region.aws.neon.tech/orbit?sslmode=require"WORKER_DATABASE_URL="${DIRECT_DATABASE_URL}"Neon's pooler is the right thing to use at runtime (serverless-friendly connection counts), but migration tooling needs a direct session for advisory locks. The setup above covers both.
Supabase
Same shape as Neon. Supabase exposes a pooler (Supavisor) on port 6543 and direct access on 5432:
DATABASE_URL="postgresql://postgres.xxx:pass@aws-0-region.pooler.supabase.com:6543/postgres?pgbouncer=true"DIRECT_DATABASE_URL="postgresql://postgres:pass@db.xxx.supabase.co:5432/postgres"WORKER_DATABASE_URL="${DIRECT_DATABASE_URL}"Supabase pooler URLs require ?pgbouncer=true — the Prisma client uses it to disable prepared statements, which PgBouncer in transaction mode doesn't support. The Drizzle postgres / pg drivers are not affected, but the flag is harmless to leave in place.
AWS RDS / Aurora
RDS doesn't front with a pooler by default — the direct URL works for both runtime and migrations. Set only DATABASE_URL, skip the others:
DATABASE_URL="postgresql://orbit:pass@orbit-prod.xxxx.us-east-1.rds.amazonaws.com:5432/orbit?sslmode=verify-full"If you're using RDS Proxy (transaction mode), add DIRECT_DATABASE_URL pointing at the cluster endpoint directly.
Railway Postgres
The in-project Postgres add-on exposes both a public DATABASE_PUBLIC_URL and an internal-network DATABASE_URL. Use the internal one for the deployed API, the public one for local ops (running migrations from your laptop, ad-hoc queries). No pooler, so the direct-URL dance doesn't apply.
Fly Postgres
Fly-managed Postgres sits inside the Fly private network. No pooler by default; reach it via top1.nearest.of.<app>.internal. The one wrinkle: Fly's backup semantics are more DIY than Neon or Supabase — read their operator docs before going to production.
When you actually need a pooler
The ORM client holds a connection per in-flight operation. Under load, a direct connection model hits Postgres's max_connections quickly — especially on cheaper plans where it's 50 or 100. A transaction pooler lets 100 ORM calls share 10 backend connections.
- Neon, Supabase, Vercel Postgres — pooler included.
- RDS, Fly Postgres, self-hosted — add PgBouncer yourself, or size the DB up enough that you don't need to.
Migrations in prod
Never run prisma migrate dev against production — it's interactive and can drop data. Use prisma migrate deploy, which only applies already-generated migrations:
npm exec --workspace=@orbit/api prisma migrate deployThe API's Dockerfile has a migrate stage that runs exactly this command; docker-compose sequences it before the API via service_completed_successfully, and Railway's preDeployCommand does the same. For anywhere else, run it as a pre-deploy step — not at container start, to avoid races between replicas.
Backups & PITR
Out of scope for Orbit, but worth saying: PITR is table-stakes the moment you have real users. Neon and Supabase bill for it; RDS does it via snapshots and WAL archival; self-hosted means running pgBackRest or similar. If your provider doesn't offer PITR on the plan you're on, upgrade before you launch.
Extensions
The stock kit uses no extensions beyond what the ORM needs. If you add pgvector, pg_trgm, or timescaledb for feature work, document them in a migration (CREATE EXTENSION IF NOT EXISTS ...) so the migrate stage provisions them on every environment.