Adapted from the repo's docs/adr/0005-filesystem-content-source-of-truth.md — Part of Internals: the platform's design set, published as content on the platform it describes.
- Status: Accepted
- Date: 2026-07-05
- Deciders: contract-owner, lead
- Tiers affected: backend, infra
- Related: ADR-0003 (environment baseline —
refines, does not supersede), ADR-0006,
ADR-0008 (read-time gating reads this
index),
../master-blueprint.md§1, §4
Context
The product authors content out-of-band as files — a recursively scanned content root
where each directory is a section (metadata.json) and each *.md file is an article with
YAML front-matter (blueprint §3). There is no admin panel
(ADR-0006), so no application code ever originates content. ADR-0003 mandates PostgreSQL 16
as the system of record and Redis for cache/queues; this decision settles what those stores
hold for content without weakening ADR-0003 for everything else. The choice is expensive to
reverse — it fixes the data-flow direction the whole backend Data layer is built around — so
it must be decided before implementation.
Decision
We will treat the filesystem as the sole source of truth for content. PostgreSQL is a
derived, fully rebuildable index (one row per article/section, with resolved state,
go_live_at, tags, content_hash) that powers listing, filtering, pagination, and search;
Redis caches rendered output (sanitised HTML keyed by content_hash, the section-tree
snapshot, aggregates). Content flows filesystem → index → cache, one direction only; the
index and cache are disposable projections that content:reindex reconstructs from disk.
Alternatives considered
- Content-in-database as SoR — a conventional CMS store. Rejected: there is no admin panel or write API to populate it (ADR-0006), and files are the authoring surface; a DB SoR would need a UI this product explicitly excludes (blueprint §1.2).
- No index — read and parse the filesystem per request — simplest, always fresh. Rejected: no efficient pagination, filtering, or full-text search; walking and parsing the tree on every request does not scale and couples every read to disk latency.
Consequences
- Easier: the index and cache are rebuildable at will — a corrupt or schema-changed index is fixed by dropping and reindexing, not by data-recovery. Content review = reading files.
- Harder / now forbidden: no code path may write content into Postgres as an origin; the index is write-only from the reindexer. A reverse edge (API mutating content rows as truth) is a violation.
- Follow-up:
data-engineerbuildsFilesystemContentSource, the index schema, and an idempotentcontent:reindex;platform-engineerwiresCONTENT_ROOTand adds acontentprobe to/health;contract-ownerpublishes the read endpoints (ADR-0006).
Compliance
- Rebuildable-from-filesystem test: drop the index, run
content:reindex, and assert the API returns byte-identical results — the index carries no state absent from disk. - Idempotence test: running
content:reindextwice with no file change is a no-op (hash-guarded; zero upserts on the second run). - One-direction review/import scan: no write to
articles/sectionsrows outside the reindex path; no controller or Domain action mutates content storage. - Health:
GET /healthexposes acontentcheck (content root readable + index fresh).