Skip to content

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

ModelsFiles
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’slibs/<product>/db/prisma/*.prisma
The composed schema (generated, never edit)libs/<product>/db/prisma/schema.prisma
Migrationslibs/<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

  1. Edit the .prisma sources (never the generated schema.prisma).

  2. Compose and create a migration:

    Terminal window
    pnpm nx run <product>-db:compose-schema
    pnpm nx run <product>-db:migrate-dev # writes a new migration and applies it
  3. Review the generated SQL and commit it with the model change. A backfill (filling a new NOT NULL column on existing rows) is hand-edited into the migration: create it with migrate-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); plain delete / deleteMany throw on these models.
  • Don’t write isDeleted: false in a top-level where (the extension adds it). Do write it in relation filters, include and nested select, 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.