ADR-0017: Documentation Central — a generated Docusaurus site on its own pipeline
Status: Accepted Date: 2026-08-04
Context
docs/ holds roughly 135 Markdown files covering architecture, backend,
frontend, security, deployment, monitoring, compliance and sixteen decision
records. It is good material and it renders on GitHub, but that is the only way
to read it: there is no search, no navigation, no way to send a link to someone
without a repository account, and no end-user documentation at all — every page
is written for a developer or an operator.
Two further gaps came from ADR-0012. The API contract exists only at runtime, so
there is nothing to link to; and SWAGGER_ENABLED is false in production, which
that ADR itself recorded as "a real gap if API access ever becomes a product
feature".
The requirement was a public documentation site at docs.motorphenterprise.com
covering eight sections plus a browsable API reference, without forking the
content: docs/ had to stay the single source of truth, editable as plain
Markdown that still renders on GitHub.
Decision
A Docusaurus 3 site in docs-site/, built from docs/ by
a staging script, published as an nginx image on a pipeline of its own.
-
docs/is untouched and remains the source of truth.docs-site/scripts/stage-docs.mjsgenerates the site's content tree on every build: it re-lays the files into the eight published sections (which deliberately do not match the repository's directory layout), rewrites every relative link, copies referenced images and PDFs intostatic/, and injects front matter on the copy only. No front matter, no MDX and no site-specific syntax ever entersdocs/.A build-time transform was chosen over pointing Docusaurus at
../docsbecause roughly 700 links indocs/point at files outsidedocs/— source files, compose files, migrations — which do not exist on a standalone site. Those become links to the file on GitHub. The script fails the build on any link it cannot resolve, which turns a class of silent rot into a build error. -
Curation is explicit. Pages that must not be public — demo credentials, the VPS hardening and secret-rotation runbook,
docs/archive/— are named in a denylist, and any page whose opening lines contain<!-- internal -->is skipped. Inbound links to an excluded page are rewritten to GitHub rather than left broken. CI greps the built output for sentinel strings so a leak fails the build rather than shipping. -
The API reference is generated, not written.
OpenApiConfigurationadds the document metadata springdoc cannot infer (title, servers, thebearerAuthscheme) and splits the ~86 controllers into nineGroupedOpenApidocuments by URL prefix. CI boots a dev-configured backend, curls each group, and renders per-endpoint pages withdocusaurus-plugin-openapi-docs. No spec is committed — a checked-in copy is stale the moment a controller changes. CI cross-checks the union of the groups against the ungrouped document and fails on any endpoint that belongs to no group, so the published reference cannot silently omit an endpoint. -
Its own image, tag, compose project and workflow. The site ships as
ghcr.io/jomariabejo/motorph-payroll-docs, deployed by.github/workflows/docs.ymlthroughdeploy/docs-deploy.shinto themotorph-docscompose project, routed by adocs.{$DOMAIN}block indeploy/Caddyfile.
Alternatives considered
GitHub Pages. Rejected on three grounds: Pages on a private repository
requires a paid plan, and this repository is private on a personal account (the
same paywall already forced codeql.yml's upload: never and the absence of
environment protection rules); a Pages custom domain would sit outside the
Cloudflare Full (strict) → firewalled-origin model every other hostname uses;
and it has no staging equivalent and no rollback, which is the entire thesis of
ADR-0014/0015.
Adding the docs image to the application stack. Rejected because
docker-compose.prod.yml shares one IMAGE_TAG across every service, so a docs
typo fix would trigger a full application promote — pg_dump, a 300-second
health gate, the lot — and a docs build failure would block a payroll release.
Documentation changes far more often than payroll code and has an entirely
different blast radius.
Keeping the vps-guide public. Originally rejected — it documented the firewall posture and secret rotation of a live box — and replaced by a stub. Revisited when the guide was rewritten as a from-scratch rebuild runbook (2026-08-04): the rewrite carries no value belonging to any host (no addresses, no hostnames, no hashes; every secret is shown as the command that generates it), and the content that makes it worth publishing is precisely the part an operator cannot rebuild without — the DOCKER-USER egress trap, the TLS-mode choice, the restore ordering. The build gate was narrowed in step: it no longer fails on firewall vocabulary, and instead fails on a firewall rule naming a routable address, which is the thing that would actually leak a host's exposure. The stub is gone and the guide is published.
Consequences
Positive
- Documentation is searchable, linkable and navigable, and the eight-section layout can be organised for readers instead of for the repository.
docs/stays plain Markdown; nobody has to learn Docusaurus to write docs.- The API reference is browsable without a running backend, which closes the
gap ADR-0012 recorded — while
SWAGGER_ENABLEDstays false in production. - A docs deploy cannot break or delay an application deploy, and vice versa. Separate lock, separate compose project, separate tag.
- Broken links and leaked internal content are build failures, not discoveries.
Negative
- A second build to keep green. The staging script is real code with real edge cases; a link pattern it mishandles fails the docs build.
- The transform is indirection. What renders is not literally what is in
docs/, so a rewriting bug shows up only on the site. The script's hard-failure on unresolvable links is the mitigation. - Rewritten out-of-tree links point into a private repository, so anonymous readers hit GitHub's sign-in page. Accepted: those links live in ADRs and internals pages whose audience is the team. Switching them to plain code spans is a one-line change in the script.
- The API reference is structure-only. 76 controllers carry
@Tagbut none carry@Operationor@Schemaprose, so pages show paths, methods and inferred schemas without descriptions. ADR-0012's annotation-hygiene warning now has a visible consequence — the gaps are published. - Spec generation costs a backend build per docs CI run, warmed by the shared buildx cache.
- Infrastructure changes land only on
main. The VPS checkout tracks it, so the Caddy block and compose file take effect on merge — the same constraint every other infrastructure change in this repository has.
References
0012-springdoc-single-contract.md— the contract this publishes; its "YAML-only configuration, noOpenAPIConfigclass" detail is superseded here0014-caddy-origin-tls-ghcr-cicd.md— the Caddy edge and GHCR pipeline this follows0015-staging-promote-pipeline.md— the build-once/promote model the docs pipeline deliberately does not share../../docs-site/scripts/stage-docs.mjs— the content transform../../deploy/Caddyfile— thedocs.site block