Managed Postgres recommendations, sizing, backups, and prod migration workflow.
Production Database
The local embedded database (PGlite) is perfect for development and single-player authoring in the Studio, but it is not suitable for a multiplayer production server. This page covers the managed-Postgres options, sizing by player count, and the production migration workflow.
Why Not PGlite in Production
PGlite is a single-process WASM build of Postgres. That means:
- It runs in-process with the game server — you cannot share it with a load-balanced admin panel or a second game server.
- On-disk corruption from a mid-write crash has no replication safety net (you'll see
pglite-server.corrupt-*folders indata/during dev — those are automatic quarantine dumps). - No managed backups, no point-in-time recovery, no connection pooling across processes.
DB_MODE=remote with DATABASE_URL in your .env.
Managed Postgres Providers
| Provider | Good For | Notes |
|---|---|---|
| Supabase | Teams already using Supabase auth | Built-in PgBouncer, row-level security, nice dashboard |
| Neon | Serverless / variable load | Auto-suspend on idle, branching for staging/prod |
| Railway | Fastest setup, small teams | Simple pricing, one-click Postgres |
| Amazon RDS | Large-scale production | Full control, VPC networking, read replicas |
| Fly.io Postgres | Co-located with game server on Fly | Low latency when the server is also on Fly |
Sizing by Concurrent Players
These are starting points — measure your tick-duration histogram and scale up before you hit overruns.
| Concurrent Players | vCPU | RAM | Storage |
|---|---|---|---|
| 100 | 2 | 4 GB | 20 GB SSD |
| 500 | 4 | 8 GB | 50 GB SSD |
| 2,000 | 8 | 16 GB | 100 GB SSD + PITR |
| 2,000+ | 16+ | 32+ GB | 250+ GB NVMe + read replica + PITR |
Connection String
Use the transaction-mode pooler URL (PgBouncer) in DATABASE_URL, not the direct-connection URL. This keeps the per-connection footprint low when the game server opens many short-lived queries during a tick.
DB_MODE=remote
DATABASE_URL="postgres://user:pass@pooler.example.com:6543/ed5?pgbouncer=true&connection_limit=20"Required Extensions
The schema uses a couple of standard extensions. Enable them before running migrations:
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";Backups
- Take a daily
pg_dump -Fcsnapshot off the database host (another VM, S3, or the provider's built-in backup). - Test a restore at least once per quarter. An untested backup is not a backup.
- Enable point-in-time recovery (PITR) for 2,000+ concurrent players.
Production Migration Workflow
Never run pnpm db:push against a production database. Use migrations:
1. Snapshot the database before every deploy. 2. Generate migrations locally from schema changes:
cd packages/db && pnpm build && npx drizzle-kit generate3. Review the generated SQL in packages/db/drizzle/ and commit it.
4. Apply with drizzle-kit migrate (or your CI/CD deployment step).
5. Verify schema version and smoke-test a small number of reads/writes before opening the flood gates.
See DEPLOYMENT.md in the repo root for the exact commands and the full production checklist.