Database
Local Postgres via Docker, migrations, seeding, and the smoke check.
Local setup
Local dev runs Postgres 17 in Docker, matching the schema and driver used in production.
The port is set once, in .env.local - DATABASE_URL reads it:
POSTGRES_PORT=5432
DATABASE_URL=postgresql://postgres:postgres@localhost:${POSTGRES_PORT}/apppnpm db:up # start Postgres on POSTGRES_PORT
pnpm db:down # stop (keeps the data volume)
pnpm db:down -v # stop and wipe all datapnpm db:up runs docker compose with --env-file .env.local, which is how
Compose picks up POSTGRES_PORT - plain docker compose up -d only reads .env,
so it always uses the default port 5432. Containers are named after the project
folder (e.g. my-app-postgres-1), so projects created from this template don't
clash on names.
Port 5432 already in use? Another project's Postgres is often already on
:5432. Set a free port in .env.local - e.g. POSTGRES_PORT=5433 - and run
pnpm db:up. DATABASE_URL follows automatically, so there's nothing else to
change. If you hit the wrong database by mistake, pnpm db:migrate tells you:
"Connected to Postgres on localhost:5432, but it has no "app" database. Is
that another project's Postgres?"
Migrations
Migrations are generated from src/db/schema.ts using Drizzle Kit and live in drizzle/.
pnpm db:generate # generate a new SQL migration from schema changes
pnpm db:migrate # apply all pending migrations (idempotent, safe to re-run)Always run pnpm db:migrate after a fresh clone or after pulling changes that include
new migrations.
Seeding
pnpm db:seedClears example_items and inserts 3 fixed demo rows. Useful for resetting to a known
state when testing the example CRUD feature. Skip if you have data you want to keep.
Smoke check
pnpm db:checkA fast, read-only probe of the full DATABASE_URL → pg.Pool → Drizzle → example_items
path. Useful after a fresh clone or when the DB is misbehaving - it confirms the
connection, driver, and schema are all wired correctly without mutating any data.
Studio
pnpm db:studioOpens Drizzle Studio - a browser-based table viewer and editor. Useful for inspecting data during development.
Production
For production, replace DATABASE_URL with your hosted provider's connection string.
Neon is the recommended option - see Neon Setup
for the setup steps. No code changes are needed; only the env var changes.
Extending with caching
This template uses Postgres for all persistence. As your app grows you may reach a point where certain workloads benefit from a cache layer - common triggers are:
- Rate limiting API routes (the most common early need)
- Caching expensive queries to avoid redundant DB round-trips across serverless invocations
- Background job queues via QStash
Upstash Redis is the natural fit for this stack. It is serverless-billed
(pay per request, not per hour), connects over HTTP so it works correctly in Vercel's
serverless environment, and has a native Vercel integration that injects env vars automatically.
The setup mirrors Neon: create a database, add two env vars
(UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN), and you're connected.
When you add it, follow the same service pattern as everything else in classes/ - a
classes/services/cache/ singleton wrapping @upstash/redis. Rate limiting drops into
proxy.ts via @upstash/ratelimit without touching any other layer.
Don't add it until you have a concrete reason to. Postgres handles far more load than most early-stage products ever generate.