Skip to content
bflo.sh

ADR-0005: Filesystem is the content source of truth; Postgres indexes, Redis caches

ADR-0005 — Accepted 2026-07-05. Part of the platform's decision record.

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

  1. 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).
  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-engineer builds FilesystemContentSource, the index schema, and an idempotent content:reindex; platform-engineer wires CONTENT_ROOT and adds a content probe to /health; contract-owner publishes 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:reindex twice 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/sections rows outside the reindex path; no controller or Domain action mutates content storage.
  • Health: GET /health exposes a content check (content root readable + index fresh).