Compare commits

...
1 Commits
Author SHA1 Message Date
bot-implementer dbd42d394d docs(adr): record PostgreSQL 18 sole canonical DB decision as ADR-005
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m9s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m42s
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Successful in 3m13s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Successful in 1m2s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m9s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 46s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 53s
2026-08-31 00:27:50 +00:00
@@ -0,0 +1,131 @@
# 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.