Database
Every product has its own Postgres database, managed by Prisma. One schema and one migration history serve development, tests, CI and production. Read this before your first schema change and before your first production deploy.
Where models live
| Models | Files |
|---|---|
| Core (users, organizations, roles, billing, …) | libs/core/db/prisma/core/*.prisma |
Shared fields (// @base: id, timestamps, isDeleted) | libs/core/db/prisma/base.prisma |
| Your product’s | libs/<product>/db/prisma/*.prisma |
| The composed schema (generated, never edit) | libs/<product>/db/prisma/schema.prisma |
| Migrations | libs/<product>/db/prisma/migrations/ |
A product model may point at a core model (user User @relation(...)); the compose step adds the back-relation. A core model never points at a product model, and a product never adds columns to a core model. Organization-scoped models: Tenant data.
Changing the schema
-
Edit the
.prismasources (never the generatedschema.prisma). -
Compose and create a migration:
Terminal window pnpm nx run <product>-db:compose-schemapnpm nx run <product>-db:migrate-dev # writes a new migration and applies it -
Review the generated SQL and commit it with the model change. A backfill (filling a new
NOT NULLcolumn on existing rows) is hand-edited into the migration: create it withmigrate-dev --create-only, edit, then apply.
pnpm nx run <product>-db:studio opens Prisma Studio. <product>-db:compose-schema-check (part of lint) fails when the committed schema is stale.
A change to a core model needs a migration in every product: run compose-schema and migrate-dev for each product and commit each migration.
Prisma clients are generated into libs/core/db/generated and libs/<product>/db/generated (gitignored); builds and tests generate them first, so a fresh clone needs no extra step.
Applying migrations
pnpm nx run <product>-db:migrate is prisma migrate deploy: it applies committed migrations only. CI and production use it. There is no db push target: SQL is meant to be reviewed and committed.
In production, migrations run as their own step before the new API version starts (the migrate service in deploy/<product>/docker-compose.yml), never inside the API process, so several API instances booting at once cannot race the migration lock. See Deploy.
Soft delete
Users, roles, organizations, memberships, units, invitations, helpdesk tickets and your // @base models are never hard-deleted. A delete sets isDeleted = true and deletedAt, and a Prisma extension adds isDeleted: false to every top-level read, update and delete, so a deleted row answers 404.
- Use
prisma.<model>.softDelete(where)/softDeleteMany(where); plaindelete/deleteManythrow on these models. - Don’t write
isDeleted: falsein a top-levelwhere(the extension adds it). Do write it in relation filters,includeand nestedselect, which the extension cannot reach. - Unique values of deleted rows (emails, slugs) stay taken.
- Data your product keeps per user without a foreign key is yours to clean up when a user is deleted:
CoreModule.forRoot({ onUserSoftDeleted })(see Your product).
Seeds
pnpm nx run <product>-db:seed creates development data and refuses production and non-local databases. See Demo data.