Files
apf_portal/docs/decisions/0006-persistence-postgresql-prisma.md
julien 8bb2d7b43f
CI / check (push) Successful in 3m1s
CI / commits (push) Has been skipped
CI / scan (push) Failing after 2m41s
CI / a11y (push) Successful in 2m12s
CI / perf (push) Failing after 2m48s
chore(deps): defer Prisma major updates pending coordinated upgrade ADR (#39)
## Summary
Renovate kept proposing Prisma 7 even though we deliberately downgraded to Prisma 6 in #3 — `nestjs-prisma@0.27.0` is incompatible with Prisma 7's driver-adapter contract, and the upgrade is non-trivial enough to warrant its own ADR rather than a silent Renovate merge.

- **`renovate.json`** — add `enabled: false` packageRule for `matchUpdateTypes: ["major"]` on `prisma`, `@prisma/*`, `nestjs-prisma`. Patch and minor bumps of the 6.x line keep flowing.
- **ADR-0006** — new "Prisma version pin: 6.x in v1" subsection records the narrowing of "latest stable major" to 6.x and the two triggers for revisiting:
  1. `nestjs-prisma` ships a release supporting Prisma 7, or
  2. We decide to drop `nestjs-prisma` for a hand-rolled `PrismaModule`.

  Either path needs its own ADR (schema, client instantiation, request-scoped lifecycle all to re-validate).

## Test plan
- [ ] Once merged, the open Prisma 7 PR can be closed (see closure comment below) and won't be recreated.
- [ ] Next Renovate run confirms no Prisma-major PR is created (check the dependency dashboard issue).
- [ ] Next patch/minor of Prisma 6.x still produces a normal grouped "Prisma" PR.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #39
2026-05-06 19:31:48 +02:00

7.0 KiB

status, date, decision-makers, tags
status date decision-makers tags
accepted 2026-04-29 R&D Lead
data
backend

Persistence — PostgreSQL with Prisma

Context and Problem Statement

The BFF owns two persistent concerns: session state (covered by Redis in the future ADR-0009) and business data owned directly by the portal — the portal is not a pure proxy. Business data is relational, multi-audience (workforce vs. external customers), audit-sensitive, and must run on-prem.

Which database engine and which data-access layer do we adopt?

Decision Drivers

  • On-prem deployment (no managed-service shortcut).
  • Multi-audience data with strict isolation between workforce and customer scopes (Row-Level Security desirable).
  • ACID transactions and a well-understood operational story (HA, backup, point-in-time recovery).
  • TypeScript-first developer experience for the BFF.
  • Long-term licensing safety — open source, no recent license-change risk (e.g. SSPL).

Considered Options

Database engine

  • PostgreSQL (latest stable major). (Chosen.)
  • MariaDB / MySQL.
  • SQL Server (would imply Microsoft licensing terms).
  • MongoDB / DocumentDB (rejected — not a fit for relational, audit-heavy business data; SSPL concerns).

Data-access layer

  • Prisma (latest stable major). (Chosen.)
  • Drizzle.
  • Kysely.
  • TypeORM.
  • Plain pg driver + hand-written SQL.

Decision Outcome

Chosen options: PostgreSQL as the engine, Prisma as the data-access layer.

The engine is pinned to the latest stable major at workspace bootstrap, and tracks the upstream support window thereafter.

Prisma is wired into NestJS via the nestjs-prisma integration, with a dedicated PrismaService extending PrismaClient and exposed through DI. Migrations are managed by prisma migrate, committed to the repository, validated in CI, and applied through a controlled deployment step (covered by a future infrastructure ADR).

Prisma version pin: 6.x in v1

The "latest stable major" rule above is temporarily narrowed to Prisma 6.x in v1. Reason: at the time of workspace bootstrap, Prisma 7 was the latest major, but nestjs-prisma@0.27.0 (the only maintained NestJS integration) is not compatible with Prisma 7 — PrismaClient instantiation throws under the new driver-adapter contract. We hit this directly in PR #3 and downgraded to Prisma 6.

Concretely:

  • prisma and @prisma/client are constrained to 6.x in package.json.
  • renovate.json declares an enabled: false rule for major bumps of prisma, @prisma/*, nestjs-prisma so Renovate stops re-proposing the upgrade until we lift the pin.

This pin is revisited (and an upgrade ADR is opened) when either of these triggers fires:

  1. nestjs-prisma ships a release that supports Prisma 7 (the wrapper catches up); or
  2. We decide to drop nestjs-prisma in favour of a hand-rolled PrismaModule (the wrapper is replaced).

Either path is a non-trivial change — schema, client instantiation, and the request-scoped lifecycle all need to be re-validated — so it warrants its own ADR rather than a silent Renovate merge.

Consequences

  • Good, because PostgreSQL is the de facto enterprise open-source RDBMS — mature HA, replication, RLS, JSONB, full-text search.
  • Good, because RLS gives a strong substrate for the workforce/customer isolation called for by ADR-0007 (identity model, future).
  • Good, because Prisma's schema-first model is a single source of truth, with type generation and a readable migration log.
  • Good, because Prisma's NestJS integration is well-trodden territory.
  • Bad, because Prisma adds a generated client and a query engine binary — operational surface to ship and version.
  • Bad, because Prisma's escape hatch for advanced PostgreSQL features (advanced CTEs, window functions, RLS policies) is $queryRaw — usable, but a reminder that not everything goes through the ORM. We will not fight Prisma when raw SQL is the right tool.
  • Neutral, because Drizzle is a credible "à la pointe" alternative; we accept the more conservative choice for now and re-evaluate at the next major architectural review.

Confirmation

  • apps/portal-bff depends on prisma and @prisma/client, both pinned to 6.x (see "Prisma version pin: 6.x in v1" above).
  • A single schema.prisma lives at apps/portal-bff/prisma/schema.prisma.
  • nestjs-prisma's PrismaModule is imported globally in the BFF.
  • renovate.json carries an enabled: false rule on matchUpdateTypes: ["major"] for prisma, @prisma/*, nestjs-prisma. The rule disappears in the same PR as the Prisma 7 upgrade ADR.
  • CI runs prisma validate and prisma migrate diff against the staging schema.
  • Database engine version is pinned in deployment manifests.

Pros and Cons of the Options

PostgreSQL (chosen)

  • Good, because mature, open source under the PostgreSQL License (permissive), broad on-prem operational tooling.
  • Good, because RLS is first-class — directly serves multi-audience isolation.
  • Good, because JSONB + relational hybrid suits BFF data shapes.
  • Bad, because operational complexity (HA, backups, tuning) must be owned — true of any on-prem RDBMS.

MariaDB / MySQL

  • Good, because operationally well-known.
  • Bad, because feature gap vs. PostgreSQL (RLS, JSON ergonomics, CTE maturity, generated columns).

MongoDB

  • Good, because the document model fits some BFF caches.
  • Bad, because business data here is relational; forcing a document store would push joins into the application — bricolage.
  • Bad, because the SSPL license raises enterprise legal complications.

Prisma (chosen)

  • Good, because schema-first declarative, strong DX, type generation, mainstream in 2026.
  • Good, because a mature NestJS integration exists.
  • Bad, because the query engine binary adds a deployment artifact.
  • Bad, because some PostgreSQL features require $queryRaw.

Drizzle

  • Good, because lighter, closer to SQL, faster, type-safe.
  • Bad, because younger (2022) — smaller enterprise track record than Prisma. Kept on the watch list for re-evaluation.

Kysely

  • Good, because elegant query-builder, no ORM magic, fully type-safe.
  • Bad, because no schema/migrations of its own — must be paired with another tool, increasing surface area.

TypeORM

  • Good, because long-standing default in NestJS history.
  • Bad, because design issues (decorator hell, runtime metadata fragility, migration story) and slowing maintenance — community momentum has shifted to Prisma and Drizzle.

Plain pg driver + hand-written SQL

  • Good, because zero abstraction; full control.
  • Bad, because every cross-cutting concern (transactions, connection pooling, type mapping, migrations) is hand-built — the very bricolage we are forbidden.

More Information