diff --git a/docs/adr/ADR-005-postgresql-18-sole-canonical-db.md b/docs/adr/ADR-005-postgresql-18-sole-canonical-db.md new file mode 100644 index 0000000..0f687bc --- /dev/null +++ b/docs/adr/ADR-005-postgresql-18-sole-canonical-db.md @@ -0,0 +1,130 @@ +# ADR-005: PostgreSQL 18 sole canonical DB + +- Status: Accepted +- Date: 2026-08-31 +- Deciders: platform stream +- References: ADR index (section 70), Architecture wiki (sections 1, 4, 9), + Technology-Stack wiki (sections 5.2, 5.4, 6, 7), Engineering-Standards wiki + (sections 24, 25, 58) + +## Context + +EPPP is a greenfield personal blogging platform owned by a single small +stream. The first visible release (v0.1) is intentionally small, but the +codebase ships explicit, versioned boundaries from day one — content types, +content blocks, page sections, themes, extensions, extension settings, +navigation, visitor preferences, database migrations, media storage, +background jobs, events and rendering — and all of these need durable, +transactional storage. + +ADR-001 already records that the platform operates a single canonical +PostgreSQL database within a modular monolith; this ADR is the focused record +of the database choice itself. There is no v1 requirement for distributed +transactions, service discovery, message brokers or independent data stores, +so the database can be one engine, one instance, one consistency boundary. + +The choice is already operationalised in the workspace: + +- `compose.yaml` pins `postgres:18.6-bookworm` as the documented runtime + target (Technology-Stack §5.2/§5.4, golden tuple §7; E00-S03-T01), with a + `pg_isready` health gate and a named `db-data` volume for persistence. +- `packages/database-postgres` is the single workspace package allowed to + import `pg` and Kysely (dependency direction `database-postgres → core + ports → Kysely/pg`, Engineering-Standards §24); domain and extension + packages never import the driver. +- The migration runner coordinates on a PostgreSQL advisory lock + (`pg_advisory_lock`, E00-S03-T04) and gates app readiness on migration + completion (E00-S03-T06). +- Extensions are the growth mechanism; extension-owned tables and migrations + live in independent chains (`eppp_extension_migrations`) sharing the same + canonical database (ADR-001, Engineering-Standards §25). + +PostgreSQL 18 is Class A in the runtime stack (fixed upstream EOL date, +Technology-Stack §5.1) with a documented support window to 2030-11-14; the +LTS strategy mandates always running the current minor, with a major upgrade +as a separate operator procedure (Technology-Stack §6). + +## Decision + +EPPP uses **PostgreSQL 18 as the sole canonical database**. One PostgreSQL 18 +instance is the only database engine in the platform; it holds all core data, +and extension-owned tables live in independent migration chains inside the +same instance. `pg`/Kysely imports are isolated to `packages/database-postgres` +— the adapter boundary every database access passes through — and no other +database engine is introduced at v1. The decision text — **PostgreSQL 18 sole +canonical DB** — matches the ADR index entry (ADR-005, section 70). + +## Alternatives + +- **SQLite** — rejected: the embedded, file-backed, single-writer model does + not fit the long-lived connection-backed server plus optional worker and + background jobs, and it offers no advisory-lock coordination for concurrent + migration runners; operational tooling for the Compose deployment model is + weaker than PostgreSQL's. +- **MySQL / MariaDB** — rejected: no v1 requirement needs their + differentiators; PostgreSQL provides the standards compliance, constraints, + JSONB and advisory locks that the migration runner and the versioned + contract model rely on, and a second SQL dialect would add cost without + benefit. +- **MongoDB / document store** — rejected: no v1 need for schemaless + documents; the platform's explicit versioned contracts (content types, + blocks, schemas validated by TypeBox/Ajv) benefit from a relational, + constraint-enforcing store. +- **Polyglot persistence (multiple engines)** — rejected: no v1 requirement + justifies the operational and cognitive cost; a single canonical database + keeps one transaction and consistency boundary (ADR-001). +- **Cloud-managed database (e.g. RDS/Aurora)** — rejected for v1: the primary + runtime model is Docker Compose with local parity; a managed service can be + revisited on evidence if hosting needs change. +- **Older PostgreSQL major / unpinned minor** — rejected: 18 is the + documented golden tuple; pinning the exact 18.6 minor makes the database + container reproducible (E00-S03-T01). + +## Consequences + +- Positive: one transaction and consistency boundary across the whole + platform; mature operational tooling (standard backups, `pg_isready` health + gates already in `compose.yaml`); advisory locks coordinate concurrent + migration runners; constraints and JSONB support the versioned contract + model; the adapter boundary keeps engine specifics contained in one package. +- Negative: a heavier operational footprint than embedded options — a + dedicated service with credentials, volume persistence and health/readiness + gating; schema changes demand migration discipline (expand/contract, never + edit a released migration — Engineering-Standards §25); the single canonical + database is a single point of failure (mitigated by volume persistence, + health gates and standard backup tooling). +- Neutral: the engine is encapsulated behind core ports, so moving to a + different engine later is a deliberate, evidence-based change rather than a + default; extensions share the canonical database with their own migration + chains. + +## Operational impact + +- A dedicated `db` service in `compose.yaml`, pinned to + `postgres:18.6-bookworm`, with a `pg_isready` healthcheck that gates app + start and a named `db-data` volume that persists across restart/recreate + (`docker compose down -v` resets it). +- Credentials come from the environment (`DATABASE_URL`, Compose + `POSTGRES_*` defaults), never embedded in the image (E00-S02-T08). +- One core migration chain plus extension-owned chains + (`eppp_extension_migrations`) run behind the advisory migration lock at + startup; readiness is reported only after migrations complete (E00-S03-T06). +- Backups use standard PostgreSQL tooling against the volume; version policy + follows the LTS strategy — always run the current minor, treat a major + upgrade as a separate operator procedure (Technology-Stack §6). +- At v1 there is no replication, multi-instance or sharding; scaling stays + vertical and the database remains the single shared store. + +## Revisit trigger + +- Revisit this ADR when a module or extension needs a genuinely different + data model (graph, search index, time-series) and there is evidence that + PostgreSQL features are insufficient — per the architecture review gates + (ADR index section 68); the adapter boundary keeps such a change contained. +- Revisit if the platform grows to multiple independent products or teams + requiring independent data stores or distributed transactions (shared with + the ADR-001 revisit trigger). +- Revisit if the golden tuple or Technology-Stack changes the database choice + or its support window — for example when PostgreSQL 18 EOL (2030-11-14) + approaches and a major upgrade becomes a scheduled operator procedure, or a + new major becomes the documented runtime target.