# SPDX-License-Identifier: FSL-1.1-Apache-2.0 site_name: AppRafter site_description: Opinionated, vertically-integrated PaaS on Kubernetes # ADR 0057 decision 8: the docs are their own deployable on their own # hostname. A path on the landing origin (`apprafter.dev/docs`) was # considered and REJECTED there — it couples the docs release cadence # to the landing's and shares one cache and one rollback unit between # two artefacts with unrelated change rates. This value is not # cosmetic: `docs/hooks/llm_export.py` bakes it into every absolute URL # in llms.txt, llms-full.txt and the per-page markdown twins, and # mkdocs bakes it into sitemap.xml, so the wrong value here publishes # the rejected design in four artefacts. site_url: https://docs.apprafter.dev/ copyright: > © 2026 AppRafter Authors. Documentation licensed CC-BY-4.0; code samples Apache-2.0. Platform core FSL-1.1-Apache-2.0. docs_dir: docs repo_url: https://github.com/apprafter/apprafter edit_uri: edit/master/docs/ # Internal artefacts that live under docs/ but are NOT part of the # published site: the release-note working file (also parsed by # release-cli.yml — excluded from the SITE, never moved), the plan # ledger, RSS measurements, and the gitignored superpowers tree. # # A markdown link INTO one of these paths resolves locally but 404s on # the published site — mkdocs logs only INFO for that case, so --strict # will not catch it. Reference these files by path in backticks, never # as a link. exclude_docs: | superpowers/ changelog/history.md measurements/ # The literate-nav fragment is nav data, not a page. literate-nav # already keeps it out of the nav and out of search, but it still # rendered at /reference/cli/SUMMARY/ and appeared in sitemap.xml — # a crawlable page that is just a link list under a "do not edit" # banner. Excluding it from the SITE does not hide it from # literate-nav, which reads the docs tree, not the build output. reference/cli/SUMMARY.md # Build hooks (see `hooks:` below). They live under docs/ to sit # beside what they act on, but they are build machinery, not # content: without this, mkdocs copies them to the site as static # files — verified, it published both hooks/cue_lexer.py and the # hooks/__pycache__/ directory CPython left there. Excluding them # from the SITE does not stop `hooks:` loading them, which reads the # path directly, not the docs tree. hooks/ # Pages deliberately reachable by URL but absent from nav. Anything # NOT on this list that is missing from nav fails the build — an # orphaned guide (node-prep.md was invisible for months) is a bug. # Only the ADR bodies are on it: `adr/README.md` is the nav entry and # indexes every record, so listing each one beside it would be noise. not_in_nav: | adr/* # NOTE: mkdocs 1.6.1's validation levels are warn/info/ignore only — # there is no literal "error" level. `--strict` (see the Justfile) is # what promotes "warn" to a build failure, so "warn" here is the # strictest real setting and is what actually enforces these checks. # `just docs-build` passes --strict; `just docs-serve` (local preview) # does not, so there a warn is only a printed line — the enforcement # point is the build, not the live-reload server. validation: nav: omitted_files: warn not_found: warn absolute_links: ignore links: not_found: warn anchors: warn unrecognized_links: warn absolute_links: warn theme: name: material # The AppRafter mark, not material's stock `material/library` glyph. # Two files rather than one: `partials/logo.html` renders theme.logo # through an , so it cannot inherit the page's colour, and the # header bar is the primary colour in both schemes — hence a # fixed-light variant. The favicon is the landing's file byte for # byte, so a browser tab cannot show two different marks for one # product. Both sit at the docs root rather than under `assets/`: # docs-site/Caddyfile pins `/assets/images/*` for 24h and gives # everything outside `/assets/` a five-minute window, and a file under # `docs/assets/` would match neither rule. logo: apprafter-logo-header.svg favicon: favicon.svg # Adds one per page (see # overrides/main.html), so an agent on a rendered page can reach the # markdown twin without guessing /llms.txt exists. custom_dir: overrides features: - navigation.instant - navigation.tracking - navigation.tabs - navigation.sections - navigation.indexes - navigation.top - search.suggest - search.highlight - content.code.copy - content.tabs.link palette: - media: "(prefers-color-scheme: light)" scheme: default primary: indigo accent: indigo toggle: icon: material/weather-night name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: indigo accent: indigo toggle: icon: material/weather-sunny name: Switch to light mode # Plausible, through mkdocs-material's own analytics integration point: # partials/integrations/analytics.html includes # partials/integrations/analytics/.html, which `overrides/` # supplies. `provider` is not a fixed enum, so no plugin is involved. # # Which property it reports to is decided in that partial and nowhere # else — the site is baked into the `pa-*.js` bundle it requests. This # one is `docs.apprafter.dev`, separate from the landing's. # # Cookieless and with no cross-site identifier, so there is no consent # banner to gate it behind, and docs-site/Caddyfile sets no CSP to widen. # `docs/hooks/llm_export.py` also writes into `extra`, which is why this # key is spelled out here rather than assumed absent. extra: analytics: provider: plausible plugins: - search # The CLI reference is generated (`just docsgen-generate`), so its # nav is generated with it: `docs/reference/cli/SUMMARY.md` is a nav # fragment docsgen emits alongside the pages. Listing 26 command # pages by hand here would be a second source of truth that drifts # the first time a command is added. - literate-nav: nav_file: SUMMARY.md # Provisioned in flake.nix for exactly this and never wired up, so # until now a rename silently 404'd the published URL — nothing in # `just docs-check` can see a URL that used to exist, because the # build only knows the tree it was given. # # An entry goes in the SAME commit that renames or deletes a page, # keyed on the OLD source path. This map is the only record that a URL # existed; there is nowhere else to infer it from. # # What it cannot do: it maps page → page, never anchor → anchor. A # section that moves to another page leaves its `#fragment` behind. # In-repo referrers are caught by `--strict` (`links.anchors: warn` # above); an external deep link lands at the top of the page it named, # and that is the accepted cost of moving a section. - redirects: redirect_maps: {} # Build hooks. mkdocs loads these by PATH (not as importable modules) # and calls the same event API a plugin gets, so a hook can do what no # packaged plugin here can — pygments ships no CUE lexer, and nixpkgs # has no llms.txt plugin (ADR 0057 §"Keep MkDocs-Material" records both # as accepted costs). # # Hooks live under docs/ so they sit beside what they act on; that also # puts them inside docs_dir, where mkdocs would otherwise copy them to # the site as static files — hence the exclude_docs entry above. hooks: # Registers a CUE lexer and FAILS THE BUILD if the registration did # not take. pymdownx.highlight catches a lexer-lookup failure and # falls back to `text`, so without that self-check a broken hook is a # green build with every ```cue fence silently unhighlighted. - docs/hooks/cue_lexer.py # Writes llms.txt, llms-full.txt and a markdown twin per page into # the built site. Nothing it writes is committed: all three derive # from committed content at build time, so they cannot drift from it. # Listed after the lexer because it captures each page's markdown and # a hook that edited markdown would have to run first to be seen. - docs/hooks/llm_export.py markdown_extensions: - attr_list # explicit {#stable-anchor} ids — the docs cross-reference gate resolves against these - admonition - pymdownx.details - pymdownx.highlight # explicit: a build hook registers a CUE pygments lexer and needs this extension present to attach to - pymdownx.superfences: # Structural concepts (the tier model, the bootstrap → Argo CD # handover, the need → claim → secret → env chain, the migration # gate) are carried as `mermaid` diagrams. Material renders these to # `
` and loads mermaid.js itself; the source
      # fence reaches the twin/bundle unchanged (it IS the diagram).
      custom_fences:
        - name: mermaid
          class: mermaid
          format: !!python/name:pymdownx.superfences.fence_code_format
  - pymdownx.snippets: # guides include REAL files instead of carrying copies
      # Snippet paths are written repo-root-relative (e.g.
      # "examples/applications/redis-app.cue"). `!relative $config_dir`
      # anchors that root to this file's directory rather than to the
      # caller's CWD — a bare `base_path: ["."]` silently depends on
      # every caller cd-ing to the repo root first.
      base_path: !relative $config_dir
      # Traversal above the base is blocked (this is the default; pinned
      # here because a repo-root base is worth being explicit about).
      restrict_base_path: true
      # A missing include is a hard build failure, not a literal
      # "--8<--" line nobody notices.
      check_paths: true
  - pymdownx.tabbed:
      alternate_style: true
  - pymdownx.tasklist:
      custom_checkbox: true
  - tables
  - toc:
      permalink: true

nav:
  - Home: index.md
  - Operator Guide:
      - Overview: operator-guide/index.md
      # Ordered by the sequence the work happens in (plan.md 2.23e), not
      # by artefact family. Each comment below says which step of that
      # sequence a page is, and a page that cannot answer that question
      # is in the wrong place.
      #
      # 1-4: stand a cluster up.
      - Quickstart: operator-guide/quickstart.md
      - Managing targets: operator-guide/target-store.md
      # 5: firewall and domains. Both are about cluster-owned resources
      # (the zones on the platform Gateway and the certificate that
      # terminates TLS for them), which is operator work.
      - Connect a domain: operator-guide/connect-a-domain.md
      - Cloudflare Origin CA cert: operator-guide/cloudflare-origin-cert.md
      # 6: backup. Moved up nine places. The page tells the reader to set
      # it up before they need it, and position 15 of 19 under a heading
      # called "Day 2, and getting out of trouble" is not before.
      - Back up a cluster: operator-guide/backup-restore.md
      - Restore from a backup: operator-guide/restore.md
      - Backup retention & integrity: operator-guide/backup-maintenance.md
      # 7: dependencies. Secrets comes FIRST of them, up eight places:
      # `backup-restore.md` already sends the reader here, and seals a
      # SealedSecret on their behalf, before they have met sealing —
      # and both `postgres` and `redis` assume sealed credentials exist.
      - Secrets: operator-guide/secrets.md
      - Postgres: operator-guide/postgres.md
      - Redis: operator-guide/redis.md
      - JetStream: operator-guide/jetstream.md
      - Persistent disk: operator-guide/persistent-disk.md
      - Shared volumes: operator-guide/shared-volumes.md
      - Shared databases: operator-guide/shared-databases.md
      - Egress policy: operator-guide/egress-policy.md
      # 8-9: reading a failure, then the platform's own lifecycle.
      - Troubleshooting: operator-guide/troubleshooting.md
      # Not a setup step: bootstrap already applies the reservations and
      # the swap (ADR 0055 decision 7), so this is the retrofit for an
      # older node, a node whose swap step failed, or reading the posture.
      - Node preparation: operator-guide/node-prep.md
      - Platform management: operator-guide/platform-management.md
      - Migration plans: operator-guide/migration-plans.md
      # Both machine pages sit last on purpose. The `target add` wizard
      # already makes the choice, so `Choosing the machine` is the
      # reference for the exceptions — a scripted target, a rejected
      # SKU, a pre-default cluster — none of which a first-time operator
      # is doing; and moving a machine is a rare rebuild off the
      # critical path that used to interrupt the backup block.
      - Choosing the machine: operator-guide/choosing-the-machine.md
      - Moving to a bigger machine: operator-guide/moving-to-a-bigger-machine.md
      - Recovery: operator-guide/recovery.md
  - How it works:
      # Mechanism, split out of the guides by ADR 0058: a guide is a
      # recipe, and an explanation of what the platform does behind a
      # step is a different job for a different reader. Sits after the
      # two guide sections because that is the order the reader meets
      # them in — run the recipe first, come here when it surprises you.
      - Overview: how-it-works/index.md
      - Declared Postgres dependencies: how-it-works/needs-pg.md
      - Declared Redis dependencies: how-it-works/needs-redis.md
      - Declared JetStream dependencies: how-it-works/needs-jetstream.md
      - Declared disk dependencies: how-it-works/needs-disk.md
      - Egress derived from declared dependencies: how-it-works/egress-policy.md
      - The target store on disk: how-it-works/the-target-store.md
      - GitOps and the CUE plugin: how-it-works/gitops-and-the-cue-cmp.md
      - Node reservations and swap: how-it-works/node-reservations-and-swap.md
      - How the approval gate works: how-it-works/the-approval-gate.md
      - What cluster-bootstrap installs: how-it-works/what-cluster-bootstrap-installs.md
      - How the platform upgrades itself: how-it-works/platform-upgrades.md
      - Cross-application shared volumes: how-it-works/cross-application-shared-volumes.md
      - Shared databases: how-it-works/shared-databases.md
      - How a restore replays a backup: how-it-works/how-a-restore-works.md
      - How retention and the integrity check work: how-it-works/backup-retention-and-checks.md
      - Per-environment deploy: how-it-works/per-environment-deploy.md
      - The image digest: how-it-works/the-image-digest.md
      - The public route: how-it-works/the-public-route.md
      - Source credentials: how-it-works/source-credentials.md
      - Sealing a secret: how-it-works/sealing-a-secret.md
      # Worked examples: a whole assembly rather than one mechanism.
      # Both are real deployments in this repository, included from the
      # manifests they describe rather than copied.
      - "Worked example: the documentation site": how-it-works/deploying-the-docs-site.md
      - "Worked example: the landing site and its CMS": how-it-works/deploying-the-landing-and-cms.md
      - Right-sizing an application's requests: how-it-works/resources-and-autoscaling.md
      - Health checks on a running application: how-it-works/health-checks.md
  - Developer Guide:
      - Overview: dev-guide/index.md
      # Ordered as the work happens (plan.md 2.23e), which for a
      # developer starts before the manifest exists.
      - Quickstart: dev-guide/quickstart.md
      # Steps 3-4: the image is the input the platform takes (ADR 0010),
      # and the moving tag is how a new build reaches it. One page,
      # because it is one story.
      - From source to a running image: dev-guide/build-and-push.md
      # What the platform then does with that tag — read before the
      # manifest names it, which is why it is above `application-cue`.
      - Image iteration: dev-guide/image-iteration.md
      # Step 5, and the three things you reach for while filling it in.
      - Writing Application.cue: dev-guide/application-cue.md
      - Deploying more than one environment: dev-guide/environments.md
      - Resources and autoscaling: dev-guide/resources-and-autoscaling.md
      - Health checks: dev-guide/health-checks.md
      - Secrets: dev-guide/secrets.md
      # Last of the "describe your application" run: the reader has now
      # met every field that can hold a change for approval, and has not
      # yet been told that some of them do.
      - When a change needs approval: dev-guide/when-a-change-needs-approval.md
      # Steps 6-7: connecting the manifest to the cluster, and the
      # credentials a private source needs to do it.
      - Private repos & registries: dev-guide/private-repos-and-registries.md
      # Step 10. Split out of `image-iteration` (2.23e): a reader looking
      # for "undo" does not know to look under "iteration".
      - Rolling back a bad deploy: dev-guide/rollback.md
  - Reference:
      - Overview: reference/index.md
      # Expanded by literate-nav from reference/cli/SUMMARY.md, which
      # docsgen writes — a new top-level command needs no edit here.
      - CLI: reference/cli/
      # Hand-written on purpose: only four of these variables are
      # declared through clap (`#[arg(env = …)]`), so the generated
      # tree cannot carry the rest — they are read by `std::env::var`
      # in command code, or by a dependency (miette, owo-colors,
      # tracing-subscriber).
      - Environment variables: reference/environment.md
  - Contributing:
      - Overview: contributing/README.md
      - Setup: contributing/setup.md
      - Writing documentation: contributing/documentation.md
      - Documentation gate: contributing/documentation-gate.md
      # The file stays under operator-guide/ so its eight inbound
      # references keep resolving, but it is listed here: it is the
      # runbook for publishing THIS project's documentation, and two of
      # its five steps edit this repository — the GHCR package visibility
      # of `apprafter/docs`, and `landing/`. Neither is an operator
      # action. `llm_export` derives `audience` from the nav group first,
      # so the machine-readable audience follows this move for free.
      - Publishing this site: operator-guide/publish-the-docs-site.md
      - License headers: contributing/license-headers.md
      # Which license covers which directory and how the FSL conversion
      # works. It was reachable by URL but absent from the nav, which
      # made it findable only by search — this is a page visitors look
      # for by name.
      - Licensing: license.md
  - ADRs: adr/README.md
  - Status: status.md