Database
Drizzle, over pg: any Postgres.
Neon on Vercel, a container, a managed
instance. Tables are prefixed id_, so one database can serve a whole
deployment.
Locally
With no database URL, the service starts PGlite (external site)(pglite.dev) —
Postgres compiled to WebAssembly — in .data/id, behind a local Postgres
socket on port 54310, so the app still talks to it through pg. PGlite would
be corrupted by two processes, so whoever holds its lock file serves it, and
the CLI or drizzle-kit studio connect to that port. It is migrated for you.
Migrations
The schema is lib/server/schema.ts. drizzle-kit writes each migration
forward; its rollback is the hand-written <tag>.down.sql beside it.
pnpm db:generate --name add-thing # the next migration, and a stub to undo it
pnpm db:migrate
pnpm db:rollback [--steps N | --to TAG]
pnpm db:status
Migrations run in the order of drizzle/meta/_journal.json, each in its own
transaction, under a lock, and each is recorded with a hash: one edited after
it ran is refused, as is running against a database that has migrations this
version does not know. A migration without a written down file cannot be
rolled back, and says so rather than half-trying.
A real database is migrated by the build that deploys it, after next build
— pnpm migrate, which is fg-dist migrate --build — or by hand with
pnpm db:migrate. Never at start-up, so a deployment never changes its schema
by surprise and a rollback is not undone by the next cold start. A Vercel
rollback promotes an older deployment without building, so every migration
has to work with the release before it as well: add a column before the code
that needs it, and drop one a release after the code stops using it.
Where the migrations are, the journal table they are recorded in
(id_migrations) and the lock they run under are declared in package.json,
under fairgarden.migrations. That is what lets a monolith migrate this app
without loading it. The migrator itself is fg-dist's, the same one the embedded
database is migrated by.