--- status: accepted date: 2026-04-29 decision-makers: R&D Lead tags: [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 - PostgreSQL: https://www.postgresql.org/ - Prisma: https://www.prisma.io/ - `nestjs-prisma`: https://nestjs-prisma.dev/ - Related ADRs: [ADR-0005](0005-backend-stack-nestjs.md), ADR-0007 (identity model + RLS use, future), ADR-0009 (sessions in Redis, future), future infrastructure ADRs (Postgres HA, backup).