This site exists because the previous one drifted far enough from the implementation to document two HTTP routes that never existed. The design response is structural, not editorial: every table that can be derived from the source is derived from the source, every published code sample is extracted from a file the test harness executes, and CI fails when either is behind what it was derived from. Prose is hand-written; facts are generated.
The stack
Astro 7 with @cloudflare/nimbus-docs, which supplies the content schemas, the sidebar and
table of contents, the MDX-to-markdown alternate for each page, and the nimbus-docs linter.
Tailwind 4 comes in through its Vite plugin. Output is static, so pnpm build emits dist/
for Cloudflare Pages. Search is Pagefind, indexed at build time. Open Graph cards are rendered
by astro-og-canvas; icons are Phosphor through astro-icon.
The content tree
- webdoc/
- astro.config.ts
- package.json
- scripts/
- gen.mjs
- gen-routes.mjs
- gen-config.mjs
- gen-metrics.mjs
- gen-proxy-routes.mjs
- gen-openapi.mjs
- gen-charts.mjs
- charts.py
- gen-snippets.mjs
- lib/
- source.mjs
- src/
- components.ts
- content.config.ts
- content/
- docs/
- start/
- use/
- reference/
- selfhost/
- internals/
- benchmarks/
- partials/
- generated/
- snippets/
- docs/
Pages are .mdx under src/content/docs/<section>/. The six directories are the six sections
in the header, each declared in astro.config.ts with autogenerate, so a new file appears in
its sidebar without any registration. The sidebar is scoped per section, which is why every
whole tree is present in every rail with groups collapsed, so a reader can see that a section
exists without having to guess: the group holding the current page opens itself.
src/content/partials/ holds fragments that pages pull in with <Render file="…" />. Both of
its subdirectories are machine-written: generated/ by five of the generators and snippets/
by the sixth. Editing a file in either is pointless: the next pnpm gen overwrites it, and
gen-snippets deletes and rewrites its whole directory so that removing a marker in the source
removes the partial.
The frontmatter contract
src/content.config.ts registers the docs collection with five fields added to the Nimbus base
schema:
---
title: Short title
description: "One sentence. Quote it if it contains a colon."
type: concept # overview|quickstart|tutorial|how-to|concept|reference|example|troubleshooting|changelog
tier: use # use|operate|internals
status: stable # stable|beta|preview|internal, defaults to stable
sidebar:
order: 3
label: Shorter label
sourceOfTruth:
- server/src/handlers/data.rs
verifiedBy:
- clients/client-js/test-v2/push.js
generated: false
---title is required and becomes the H1, so the body must not repeat it. type names the recipe
the page follows: one page, one reader question. tier marks which reading tier it belongs to.
status renders a badge for anything other than stable, so a reader never mistakes a preview
for a contract.
The last three are the traceability contract, and they are the part that matters most.
sourceOfTruth lists the repo-relative paths of the code that governs the page: if that code
changes, the page is in debt, and the list is what tells you which pages to re-read.
verifiedBy lists tests or examples that execute what the page shows. A page claiming
“verified” has to name what verified it. generated marks machine-written content.
Undeclared keys do not silently pass: nimbus/frontmatter-shape is configured as an error, so
a stray field fails pnpm lint:docs.
Body rules
Enforced by the linter, with every structural rule set to error in astro.config.ts:
| Rule | What it catches |
|---|---|
nimbus/frontmatter-shape |
an undeclared or malformed frontmatter key |
nimbus/internal-link |
a link to a page path that does not exist |
nimbus/description-required |
a page with no description |
nimbus/single-h1 |
an # heading in the body, competing with the title |
nimbus/heading-hierarchy |
a skipped heading level |
nimbus/code-block-lang |
a fenced block with no language tag |
nimbus/code-block-prompt-prefix |
a shell block written with a $ prompt |
nimbus/no-self-host-url |
an absolute link back to this site’s own domain |
nimbus/image-ref |
an image reference that does not resolve |
Three more are warnings rather than errors: nimbus/duplicate-heading-text,
nimbus/heading-punctuation and nimbus/bare-url.
Beyond the linter, two MDX facts bite regularly. Components must be PascalCase and
registered in src/components.ts. The registered set is Accordion, AccordionContent,
AccordionGroup, AccordionTrigger, Aside, Badge, Card, CardGrid, Code, CodeGroup,
FileTree, Frame, LinkButton, LinkCard, PackageManagers, Render, Step, Steps,
TabItem and Tabs, and nothing else is available without importing it. And remark or rehype
plugins attached through the MDX integration are dropped, which is why there is no Mermaid on
this site: diagrams are prose, tables or nothing.
The seven generators
pnpm gen runs scripts/gen.mjs, which shells out to each generator in turn.
pnpm gen:check runs the same seven with --check, which compares instead of writing and exits
non-zero on any difference. Every generator writes through emitPartial, which stamps a banner
naming the sources it read and the command to regenerate.
| Generator | Partial | Derived from |
|---|---|---|
gen-routes.mjs |
generated/broker-routes |
the Router::new() chain in server/src/main.rs, the access table in server/src/auth.rs, and whether each handler takes the Tenant extension |
gen-config.mjs |
generated/broker-config |
every env_bool / env_int / env_f64 / env_str call site in server/src/config.rs, plus the raw std::env::var reads in server/src/fusion.rs |
gen-metrics.mjs |
generated/broker-metrics |
the Prometheus exposition assembled in server/src/metrics.rs and server/src/handlers/status.rs |
gen-proxy-routes.mjs |
generated/proxy-route-classes |
the request classifier in proxy/src/routes.rs, applied to the broker’s real route list |
gen-openapi.mjs |
generated/openapi-coverage, plus public/openapi/queen-{broker,proxy}.json |
both routers, each handler’s Query<T> extractor and body struct, the StatusCode variants each handler can return, and the access table |
gen-charts.mjs |
public/charts/*.svg |
the archived CSV and stdout artifacts under benchmark-queen/, plotted by charts.py |
gen-snippets.mjs |
snippets/* and snippets/index |
marked regions of the client test suites |
Routes
The router chain is sliced out of main.rs between let app = Router::new() and
.with_state(state);, and each .route("…", …) is paired with the HTTP verbs in the method
router that follows it. Tenant scoping is not transcribed either: the generator walks every
.rs file under server/src, collects the signature of each pub async fn, and marks a route
tenant-scoped when its handler takes tenant::Tenant or Extension<Tenant>.
Two sanity gates protect against a silently broken parser. Fewer than 40 parsed routes is a
hard error, and so is a handler that the router references but that does not exist under
server/src, which is precisely the class of bug that produced the two imaginary routes on the
old site.
Configuration
Defaults are read out of the call site, including nested ones. A nested call is how the source expresses a legacy alias:
env_int("QUEEN_MESH_PORT", env_int("QUEEN_UDP_NOTIFY_PORT", 6633))The generator reads that as variable QUEEN_MESH_PORT, also read as
QUEEN_UDP_NOTIFY_PORT, with a real default of 6633. Trailing Rust noise (.max(1),
as usize, .to_string()) is stripped. Fewer than 60 parsed variables is a hard error.
Three curated lists shape presentation only, never content. GROUPS decides which section a
variable appears under; an unclassified variable still appears, in Other, so a new knob cannot
ship undocumented. EXPERIMENTAL and INERT move a variable into its own labelled table:
EXPERIMENTAL for knobs the source itself calls experiment-only, INERT for variables the
broker still parses and still prints in its boot log while wired to nothing. Both are still
published, because silence about a live-looking variable is how the previous site went
stale.
Metrics
The exposition is built in two places with two different shapes, so both are parsed: helper
calls in metrics.rs, and raw # HELP / # TYPE strings in handlers/status.rs, some of them
templated over a nearby array of family names. Then every "queen_*" string literal in either
file is checked against what was parsed. An unmatched family is reported on the console and
published with no help text rather than dropped. The failure mode is a visibly incomplete row,
never a missing one. Fewer than 25 families is a hard error.
Proxy route classes
The proxy is the enforcement point for what a tenant may call, and
proxy/src/routes.rs classifies every broker-bound request into exactly one class, with
anything API-shaped that matches no rule blocked. This generator applies that same
classification to the broker’s real route list, so the published table is a join of two derived
facts rather than a hand-maintained list.
One detail worth knowing if you extend it: the proxy classifies concrete paths, while the router
declares :param patterns. The generator substitutes a representative value for each parameter
before classifying, so prefix rules match the way they will at runtime.
OpenAPI documents
gen-openapi.mjs emits public/openapi/queen-broker.json and public/openapi/queen-proxy.json
as OpenAPI 3.1, plus a small coverage partial. Paths and methods come from the routers; query
parameters from each handler’s Query<T> extractor, walking the struct’s serde renames and
optionality, or from the literal keys a Query<HashMap<..>> handler reads by name; request
bodies from the struct the handler passes to serde_json::from_slice, resolved recursively;
response status codes from every StatusCode variant reachable in the handler body.
Two things resist derivation and are marked rather than guessed. Success bodies on the hot paths
are assembled by string concatenation, so their schema is left unconstrained, deliberately
not type: object, since some of those routes answer with a top-level array and asserting
“object” would be a false statement. And some listing handlers forward their whole query map to
a stored procedure, so their parameters cannot be recovered from Rust. Both cases carry an
x-queen-* extension saying so; see OpenAPI documents.
Before writing, the generator runs a structural self-check over both documents: duplicate
operation ids, path parameters that are templated but not declared, dangling $refs and
Rust-style :param leftovers all abort the run. That check earned its place immediately: it
caught /console and /console/ collapsing onto one operation id on the first run.
Benchmark figures
A chart is a claim, so the figures are derived like everything else:
gen-charts.mjs shells out to charts.py, which reads the archived CSV and
stdout artifacts under benchmark-queen/ and renders one SVG per theme into
public/charts/. --check re-renders into a temporary directory and compares, so
a changed artifact fails CI until the figure is regenerated. Rendering needs
python3 with matplotlib; the workflow installs it.
Three rules the figures follow, each of which caught a real mistake during the first pass:
- Never a second y-axis. Two measures of different scale become two stacked panels sharing an x-axis. The 24-hour figure would otherwise have plotted broker memory against PostgreSQL CPU on one plot and invented a correlation.
- A chart has to show what its caption claims. The first throughput-ceiling figure plotted offered against accepted to show a 0.086% shortfall. But at per-second resolution the two lines sit exactly on top of each other, and the shortfall is a cumulative total. It became a single-series chart, and the shortfall stayed a number in the table where it belongs.
- Check whether a counter is cumulative. The cell figure’s 429 series came
straight from
err_429, which is a running total: it plotted as a perfectly straight ramp labelled “per interval”. It is differenced into a rate now.
Two SVGs are written per figure and swapped by CSS on data-mode, because an
<img> cannot inherit currentColor and a figure tuned for a white surface is
unreadable on a black one. Palettes are the categorical slots from the
data-visualisation guidance, stepped per mode and validated against this site’s
actual surfaces rather than flipped.
Snippets
A snippet is a region of a real source file between two markers:
// docs:start(push-one-message)
// ... the code a page will publish ...
// docs:endMarker syntax is comment-flavour agnostic: anything ending in docs:start(<id>) opens a
region and anything ending in docs:end closes it, so //, # and -- all work. Each region
becomes a partial carrying a header that names the file it came from, and pages include it with
<Render file="snippets/<id>" />. A published snippet is therefore never a transcription; it is
the code that ran.
Ids must be unique across all sources and regions must be closed. Both are hard errors, as is
a docs:start inside an already-open region. Common leading indentation is stripped so a
snippet extracted from inside a test function reads as top-level code. An index partial lists
the whole inventory.
The scanned directories are the JavaScript, Python, Go and C++ client test trees.
The drift guards
Two of the generators cannot work by parsing alone: they need to apply a Rust rule, so they mirror it in JavaScript. A stale mirror would not fail. It would quietly publish a wrong table, which is the worst possible outcome. So each mirrored function is fingerprinted.
fingerprint() strips line and block comments, collapses all whitespace, hashes with SHA-256
and keeps the first 16 hex characters. Comment edits and reflows therefore do not trip the
guard; a changed rule always does. assertFingerprint throws with the expected value, the
actual value and instructions to re-read the Rust, update the mirror, and only then paste the
new fingerprint in.
| Mirrored function | Guarded in |
|---|---|
route_access_level in server/src/auth.rs |
gen-routes.mjs (and re-derived in gen-openapi.mjs) |
classify in proxy/src/routes.rs |
gen-proxy-routes.mjs |
is_operator_route in proxy/src/routes.rs |
gen-proxy-routes.mjs |
When a guard trips, the generator refuses to run at all. That is the intended behaviour: the build stops until a human has compared the two implementations.
Commands
| Command | What it does |
|---|---|
pnpm dev |
Astro dev server |
pnpm gen |
run all seven generators, writing partials, the OpenAPI documents and the figures |
pnpm gen:check |
compare instead of writing; non-zero on drift |
pnpm lint:docs |
the Nimbus linter over prose, MDX and frontmatter |
pnpm lint:docs:fix |
the same, applying the fixable rules |
pnpm build |
static build into dist/, including the Pagefind index |
pnpm typecheck |
astro check |
pnpm verify |
gen:check, then build, then lint:docs (what to run before pushing) |
Run them from webdoc/, or with pnpm --dir webdoc <script> from the repository root.
In CI
.github/workflows/docs.yml runs pnpm gen:check, then pnpm build, then pnpm lint:docs, so
a page built from source can never be published behind that source. The order matters: the
nimbus/internal-link rule resolves links against .nimbus/routes.json, which Astro
materializes at astro:build:done, so linting before building checks links against a stale
route map, or none at all on a clean checkout.
Its path filter is the other interesting part: because the generators read code, the workflow
triggers on changes to server/src/**, server/sql/**, the proxy’s router, console and auth
modules, and the JavaScript, Python and Go client test directories, not only on changes under
webdoc/**.
The workflow builds and gates; it does not deploy. Deployment is Cloudflare Pages against
dist/.
Adding a page
-
Read the code the page will describe, and list those paths in
sourceOfTruth. This is the step the old site skipped. -
Create the
.mdxunder the right section directory. The sidebar picks it up fromautogenerate; setsidebar.orderto place it. -
Prefer a generated partial to a hand-written table, and a verified snippet to hand-written code. If a flow you want to show has no snippet, consider adding markers to the test that already covers it rather than transcribing it.
-
Check the whole contract in one command.
pnpm --dir webdoc verify