CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m7s
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Successful in 3m8s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Successful in 1m4s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m11s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 45s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m39s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 50s
131 lines
7.0 KiB
Markdown
131 lines
7.0 KiB
Markdown
# 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.
|