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 thewebcontainer 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
- Repo-root
content/directory, bind-mounted — keeps the example tree insideplatform-engineerownership and out ofbackend/. Rejected: it diverges from Laravel'sstorage/appconvention and splits content away from the tier that reads it; the owner (data-engineer, who buildsFilesystemContentSource) should own the tree's location. - 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.
- 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, sodata-engineer'sFilesystemContentSourceroots naturally atCONTENT_ROOT. - Harder / now scoped elsewhere: the content tree and the app-side config that reads
CONTENT_ROOT(config/content.php/ a filesystemscontentdisk) live underbackend/and are data-engineer's, not platform's; platform only wires the env var. The/healthcontentcheck that proves the wiring is backend-api's. - Open sub-decision (flagged, not resolved here): Laravel's default
storage/app/.gitignoreignoresstorage/app/*, so an example content tree placed there is not git-tracked by default. Whether the fixture tree is committed (add a.gitignorenegation) or treated as runtime data synced out-of-band is the per-environment sync question this ADR defers —data-engineerdecides 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—/healthcontent check (X-1);platform-engineer— a deployment/sync ADR once a target exists.
Compliance
CONTENT_ROOTis present inbackend/.env.exampleand resolves inside thewebcontainer:ddev exec test -r "$CONTENT_ROOT"passes once the tree is provisioned.GET /healthexposes acontentcheck (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 onFilesystemContentSource).