Skip to content
bflo.sh

ADR-0009: Content sync mechanism and CONTENT_ROOT location

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

Adapted from the repo's docs/adr/0009-content-sync-and-content-root.md — Part of Internals: the platform's design set, published as content on the platform it describes.

  • Status: Accepted
  • Date: 2026-07-05
  • Deciders: platform-engineer (drafted); contract-owner (steward — numbers/ratifies); lead + user (decision)
  • Tiers affected: infra, backend
  • Related: ADR-0003 (environment baseline), ADR-0005 (filesystem is content SoR), ADR-0008, ../master-blueprint.md §7, §12.1

Context

The filesystem is the content source of truth (ADR-0005): a single author writes Markdown + metadata.json out-of-band, and the backend reads a content root and projects it into the Postgres index and Redis cache. That leaves one wiring question the Data layer is built around and which therefore cannot be deferred past bootstrap: where does the content root live, and how does it reach the running backend in each environment? (blueprint §12.1).

Constraints bounding the options:

  • The backend reads the root through a single env var, CONTENT_ROOT, so the value is an environment concern, not a code concern.
  • Locally the runtime is DDEV (ADR-0003): the repository is already bind-mounted read/write into every container at /var/www/html, so a path inside the project tree is visible to the web container with zero extra infrastructure.
  • There is no deployment target in this skeleton (ADR-0004; architecture.md → Environments), so a production sync mechanism would be speculative today.

Decision

We will set CONTENT_ROOT=storage/app/content — a path relative to the Laravel app root (backend/), i.e. backend/storage/app/content/. Under DDEV it resolves inside the existing bind mount (/var/www/html/backend/storage/app/content), so authoring is done by editing files on the host in a normal editor and they are immediately visible to the backend — the local "sync mechanism" is the DDEV bind mount, nothing more. CONTENT_ROOT is wired in backend/.env.example (owned as topology binding by platform-engineer) so any future environment can repoint it without a code change. The cross-environment sync mechanism (git-pull-on-deploy vs. mounted volume vs. object-storage sync) is explicitly deferred to a follow-up infra ADR, to be decided when a deployment target is introduced.

Alternatives considered

  1. Repo-root content/ directory, bind-mounted — keeps the example tree inside platform-engineer ownership and out of backend/. Rejected: it diverges from Laravel's storage/app convention and splits content away from the tier that reads it; the owner (data-engineer, who builds FilesystemContentSource) should own the tree's location.
  2. Object-storage / S3 sync now — production-shaped. Rejected: there is no deploy target yet (ADR-0004); building sync before an environment needs it is premature and untestable.
  3. Hardcode the path in application config — simplest. Rejected: an env var costs nothing and keeps the root portable across environments, which is the whole point of §12.1.

Consequences

  • Easier: local authoring needs no infrastructure — edit files in the mounted tree and the reindexer (content:reindex, scheduler tick) picks them up. The path follows Laravel storage conventions, so data-engineer's FilesystemContentSource roots naturally at CONTENT_ROOT.
  • Harder / now scoped elsewhere: the content tree and the app-side config that reads CONTENT_ROOT (config/content.php / a filesystems content disk) live under backend/ and are data-engineer's, not platform's; platform only wires the env var. The /health content check that proves the wiring is backend-api's.
  • Open sub-decision (flagged, not resolved here): Laravel's default storage/app/.gitignore ignores storage/app/*, so an example content tree placed there is not git-tracked by default. Whether the fixture tree is committed (add a .gitignore negation) or treated as runtime data synced out-of-band is the per-environment sync question this ADR defers — data-engineer decides at provisioning time (task P0-5); a later infra ADR settles non-local environments.
  • Follow-up owners: data-engineer — provision the example tree + config binding (P0-5); backend-api/health content check (X-1); platform-engineer — a deployment/sync ADR once a target exists.

Compliance

  • CONTENT_ROOT is present in backend/.env.example and resolves inside the web container: ddev exec test -r "$CONTENT_ROOT" passes once the tree is provisioned.
  • GET /health exposes a content check (content root readable) — the runtime proof that the wiring holds (ADR-0005 Compliance).
  • No code hardcodes a content path; all filesystem access to content goes through CONTENT_ROOT (import/review scan on FilesystemContentSource).