[E01-S01-T04] ADR: PostgreSQL #408
@@ -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.
|
||||
Reference in New Issue
Block a user