Postgres, reached through Drizzle. Neon is what this repo was verified against, and nothing in the code is Neon-specific: any Postgres with a connection string works, including one running on your machine.
You will leave with a connection string, a database name, and a migrated database.
During the agent-run setup, the agent authenticates the current neon CLI with your handoff, inventories projects, and can create one with neon projects create. It then links the project and obtains the Postgres environment through neon env pull. Neon calls the direct value the unpooled URL; the agent maps that value to this repository's DATABASE_URL_DIRECT name.
-
Create a project
The agent creates or reuses a project after confirming the account, project, and region. You handle Neon login and choose between plausible projects or regions. The browser fallback is console.neon.tech. Pick the region closest to where your api will run, because every query pays that round trip.
Neon names the initial database
neondb. That name is the bootstrap database, and it is not where this app's tables go. -
Copy the pooled connection string
From the project's connection details, copy the pooled connection string. It has
-poolerin the host.# apps/api/.env DATABASE_URL=postgresql://user:password@ep-...-pooler.region.aws.neon.tech/neondb?sslmode=verify-fullThe pooled endpoint was verified to handle migrations, transactions, advisory locks and named prepared statements alike, so the pooled and direct split in this repo is convention rather than a workaround.
If you want the direct endpoint for migrations, copy that too. It is read only by
drizzle.config.ts, and leaving it unset falls back toDATABASE_URL:DATABASE_URL_DIRECT=postgresql://user:password@ep-....region.aws.neon.tech/neondb?sslmode=verify-full -
Name your databases
DB_NAME=postflare DB_NAME_DEV=DB_NAMEis the production database.DB_NAME_DEVdefaults to${DB_NAME}_dev, so leaving it empty gives youpostflare_devwithout declaring a name you will never connect to.The database in the connection string's path is never the target.
resolveDatabaseUrl()rewrites that path segment before anything connects, so the pasted string is a route to the server rather than a choice of database. -
Migrate
pnpm db:migrateIt creates the target database if it is not there, connecting through the bootstrap database to do it, then applies every migration under
packages/shared/drizzle/. It prints which database it reached and how.Selection is
NODE_ENVand it fails toward dev, so that command creates and migratespostflare_dev. Production is a separate, deliberate invocation:NODE_ENV=production pnpm db:migrate -
Confirm which database the api resolved
curl http://localhost:5001/healthThe
databasefield is the resolved name. It is the name and not the URL, because the URL carries a password.
Dev and prod are two databases, not two branches
Neon recommends branches for environments, and this repo deviates from that on one axis: an artifact other people clone.
A branch is two opaque ep-... connection strings a buyer must never confuse. A database name is one string plus a rule. Neither isolates the blast radius on the Free plan, because compute and storage are metered per project and a branch draws on the same pool, so the thing a branch would have bought is not available at that tier anyway.
The reasoning, including the numbers and what a separate project would buy instead, is in docs/adr/0023-two-databases-in-one-neon-project.md.
NODE_ENV=production is set on production API processes by most hosting platforms, including preview deploys. So a staging deploy writes to the production database until you give it a dedicated variable. Boot logs the resolved database name, so a wrong resolution is visible in the first lines of a deploy log, and GET /health reports it afterwards.
What is not automatic
Nothing migrates at boot. Drizzle's migrator takes no advisory lock, so two instances migrating at once keep the data consistent but the loser exits 1, which is half a deploy dead. pnpm db:migrate is a deploy step you add.
Scale to zero is fixed at five minutes on the Free plan and cannot be disabled. It lands on the dashboard and checkout only: prices come from the static catalog and the docs come from the web server bundle, so the marketing pages never touch Postgres and the cold start is not an SEO problem.
TLS is not asserted in code. The connection string is expected to carry sslmode=verify-full, and pg@8.23 warns that require weakens in pg@9. It is not enforced, because pointing this at a local Postgres with no TLS to verify against is a legitimate thing to do.
Keys you now hold
| Value | Goes in | As |
|---|---|---|
| Pooled connection string | apps/api/.env |
DATABASE_URL |
| Direct connection string | apps/api/.env |
DATABASE_URL_DIRECT, optional |
| Production database name | apps/api/.env |
DB_NAME |
| Development database name | apps/api/.env |
DB_NAME_DEV, optional |
Related
- Database schema, every table and column
docs/adr/0009-drizzle-v1-on-neon-postgres.mddocs/adr/0023-two-databases-in-one-neon-project.md- Neon CLI
