Migrations
The database is declarative: the schema in packages/database/prisma/schema.prisma is the source of truth, and every change reaches Postgres via a versioned migration generated by Prisma.
The loop
Section titled “The loop”- Edit the schema. Update
packages/database/prisma/schema.prisma— add a model, add a field, add an index, etc. - Generate the migration. From the repo root:
(equivalent to
Terminal window pnpm run db:migrate:devnpx prisma migrate devinpackages/database). This creates a new directory underpackages/database/prisma/migrations/<YYYYMMDDHHmmss>_<snake_case>/with the SQL. - Commit the migration. Both
schema.prismaand the generated migration directory must land in the same PR. - Regenerate the clients.
This refreshes the Prisma client every app imports through
Terminal window pnpm run db:generate@repo/database. - Update consumers in the same PR. If a field was renamed or removed, every consuming app —
apps/api,apps/pool,apps/ops,apps/debug— must be updated before the PR can merge. Type errors at build time are the gate.
Non-interactive migration application
Section titled “Non-interactive migration application”In CI and production environments, use:
pnpm run db:migrateThat maps to prisma migrate deploy under the hood — non-interactive, no schema regeneration prompts.
Naming convention
Section titled “Naming convention”Prisma generates <UTC timestamp>_<snake_case_description>. Two soft rules apply:
- Keep the snake-case description short and intent-revealing:
add_candidate_phone_country_code,drop_application_legacy_status,index_audit_log_target_type. - Each migration does one logical thing. Splitting a rename into a multi-step migration (add column → backfill → drop old column) is encouraged when the change must be safe under live writes.
The hard rule
Section titled “The hard rule”Never modify the database manually. No ALTER TABLE in psql, no prisma db push --accept-data-loss, no hot-fix migration files. The migration directory is append-only history; once a migration is committed, the next change is a new migration on top of it.
Renames and removals
Section titled “Renames and removals”When a column is renamed or removed, prefer a sequence of safe migrations over a single destructive one:
- Add the new column.
- Backfill old → new (in a separate migration or a one-shot script).
- Update every consumer to read/write the new column.
- Drop the old column in a follow-up migration once no consumer references it.
For an example, see the location → (locationCountryCode, locationState, locationCity) transition in CandidateProfile and JobPosting: the legacy location column is still present with a // Legacy free-form location string… comment until the cut-over completes.
Generated client and types
Section titled “Generated client and types”- The generated Prisma client is the only typed surface — apps import
Prisma,PrismaClient, model types, and enum types from@repo/database. - Do not hand-write TS interfaces that duplicate Prisma types.
- After a schema change, run
pnpm run db:generateso downstreamcheck-typessees the new shape.