Skip to main content

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.mjs generates 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 into static/, and injects front matter on the copy only. No front matter, no MDX and no site-specific syntax ever enters docs/.

    A build-time transform was chosen over pointing Docusaurus at ../docs because roughly 700 links in docs/ point at files outside docs/ — 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. OpenApiConfiguration adds the document metadata springdoc cannot infer (title, servers, the bearerAuth scheme) and splits the ~86 controllers into nine GroupedOpenApi documents by URL prefix. CI boots a dev-configured backend, curls each group, and renders per-endpoint pages with docusaurus-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.yml through deploy/docs-deploy.sh into the motorph-docs compose project, routed by a docs.{$DOMAIN} block in deploy/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_ENABLED stays 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 @Tag but none carry @Operation or @Schema prose, 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