The instance config: config/status.yaml
One YAML file per deployment. The engine (src/) is estate-agnostic:
everything it knows about an estate — the probed surfaces, the
vantages, the site identity, the mail declaration — lives in this one
file. Adding a monitored service is a config edit, never an engine
change.
The instance model: one checkout + one wrangler.toml + one YAML =
one estate's status site. npm run gen (which rides every gate)
parses and validates the YAML through scripts/load-config.ts and
emits src/generated/config.ts, consumed by the engine through the
single import point src/config.ts. An invalid config fails gen — a
bad instance never reaches a deploy.
CONFIG=config/fixtures/minimal.yaml npm run gen # build any instance
CONFIG=<path> npm run dev # run any instance locally
The doctrine the schema enforces (every rule names itself in its error):
- Strict keys everywhere. A typo'd key is an error naming the allowed set, never a silent no-op.
- Secret VALUES never live in config.
secretHeadersNAME a Worker secret (wrangler secret put); the worker resolves the value from its bindings at run time. Anything shaped like an inline value is refused. - A green means the user's path works. Every service carries a
content assertion;
route-answers(the exercised deep probe) only rides a service that also carries arequestposture. - The home vantage is structural. Exactly one, with id
home(the store's row-key convention: home rows are the un-prefixedsvc:<id>ones). - URLs are absolute https.
The blocks
site — the identity the chrome renders
| Field | Type | Rule |
|---|---|---|
name |
string, required | The shell's wordmark and the <title> suffix; the mail subjects carry it. |
base_url |
https URL, required | The canonical public URL (the wrangler STATUS_BASE_URL var's twin; the var wins at run time). |
description |
string, required | The page's meta description and the board's one-line intro. |
logo.light / logo.dark |
https URLs, optional (as a pair) | The light/dark logo pair, swapped on the .dark class. Canonical URLs, never vendored. Absent: the wordmark alone. |
links[] |
{label, href}, optional |
The chrome links (footer; the first is also the header's front-door icon). |
source_url |
https URL, optional | Where the service's source lives (the incident rows' home). |
fonts_url |
https URL, optional | The font stylesheet; absent = the house stack. The whitelabel font knob. |
docs_url |
https URL, optional | The externally hosted manual's home — linked from the public footer and the admin header when set. |
theme |
map, optional | Design-token overrides (--accent, --ink, ...). Keys are CSS custom-property names; values hex or rgb()/rgba() colors ONLY — anything that could carry CSS is refused. Applied after the compiled stylesheet. |
uptime_windows |
1-3 day counts, optional | The windows the uptime bars + summary's uptime<N>d keys render (each 1-365; default [30, 90]). The history retention becomes max(90, largest window). |
probe_log_hours |
integer, optional | The probe leg log's hour window (default 24; 0 turns the log off; the sane ceiling is 168). The per-service rows (leglog:<id>, hard-capped at 720 entries) feed the console's Probe log register — what the prober saw, per leg and vantage. Each round writes one row per service: on the free plan's write budget, set 0. |
locale |
BCP-47 tag, optional | The declared locale, rendered as <html lang> on the public page and the admin (default en-US). Display only. |
fonts_url |
https URL, optional | The stylesheet override. ABSENT (the flagship posture) the compiled stylesheet serves the SELF-HOSTED estate fonts (OFL woff2 under /fonts/, wrangler assets): zero third-party font requests, robust everywhere. |
theme (font keys) |
font stacks, optional | Exactly --font-sans, --font-serif, --font-mono may carry font stacks (family names, quotes, commas, spaces only — anything else could carry CSS and is refused); every other theme key stays hex/rgb-only. The flagship instance sets the Ribose identity: the cyan brand scale, Source Sans 3, Inconsolata. |
strings |
map, optional | Chrome-word overrides for the FIXED key set — level.operational/degraded/down/unknown, phase.investigating/identified/monitoring/resolved/ongoing, feed.maintenancePrefix. Unknown keys are refused. Testids and API enums stay canonical English: display only, never the contract. |
admin — the SaaS surface's presence (optional)
admin: false (or admin: {enabled: false}) closes the admin
interface; absent = enabled. session_ttl_hours (default 8, at most
168) sets the admin session cookie's lifetime. With no oidc block
the gate is the OPS_TOKEN secret — unset, the whole surface stays
closed (404) whether or not the flag is set. See docs/api.md for
the surface itself, and docs/manual/03-admin-guide.md for the
operating guide.
admin.oidc moves the HUMAN sign-in to an OpenID Connect provider
(the Ribose identity service in our estate, any conformant OP for a
whitelabel deployment) — by protocol, never hardcoded endpoints:
| Field | Type | Rule |
|---|---|---|
issuer |
https URL, required | The ONLY OP fact in config; every endpoint (authorization, token, JWKS) comes from /.well-known/openid-configuration at run time, cached 10 minutes. |
client_id |
string, required | The public client the operator registered with the OP. PKCE S256 covers it — no client secret exists here. |
admins_claim |
claim name, optional | The ID-token claim the OP marks administrators with (truthy = admitted). Omit when the subjects list is the whole truth. |
subjects |
list of sub values, optional |
The explicit allowlist — an admitted fallback that needs no claim. |
Access = the verified ID token's sub is listed, or its
admins_claim is truthy. With oidc set the login page offers
"Sign in with the identity service" (the token form hides); the
write API's Bearer stays OPS_TOKEN either way — machines have no
browser. The redirect URI to register at the OP is exactly
<base_url>/admin/oidc/callback.
org — the tenant this deployment serves (optional; the SaaS org model)
Declares WHO this deployment belongs to and boots the membership
model. Requires admin.oidc (memberships are keyed by the identity
service's sub; an org without the identity login is meaningless —
the loader refuses it).
| Field | Type | Rule |
|---|---|---|
id |
kebab-case | The tenant id; also this deployment's slug in the page registry (page:<id>, self: true). |
name |
string | The display name on the admin's org card. |
owners |
list of sub values, non-empty |
The BOOTSTRAP FLOOR: these identity-service subjects are always owners — KV rows can never lock them out; removal is a config edit, never an admin click. |
With org set, admin access = membership: a config-floor owner, a
member row, or a pending invite whose email matches the ID token's
email claim WHILE the OP asserts email_verified: true (a signed
token carrying an unverified email never redeems — the only
identity-matching act, on verified claims only). Roles gate the
surface: operators publish (drafts, incidents, the update trail);
owners administer (members + invites, the page registry,
backup/restore, subscriber deletes). The org's rows (org:, the
member/invite rows, page:) ride in backups. See
docs/manual/03-admin-guide.md.
groups — the ordered display groups
A non-empty list of unique strings. Every service's group must
resolve here; the page renders the groups in this order.
vantages — every location the estate probes FROM
| Field | Type | Rule |
|---|---|---|
relay_stale_ms |
positive integer, required | A relay silent past this window reads UNKNOWN on every card, never green (fail-closed). |
list[].id |
kebab-case, unique | Stable id; the home vantage's id must be exactly home. |
list[].kind |
home | relay |
Exactly one home (this worker's own probes). Relays report over the guarded /api/vantage-report. |
list[].label / list[].description |
strings | The card's vantage strip copy. Geography is observed (the relay's self-reported colo), never claimed. |
A relay that is not deployed stays commented out — never expect a report that cannot come.
services[] — the probed surfaces
| Field | Type | Rule |
|---|---|---|
id |
kebab-case, unique | The KV row key is svc:<id>. |
name / group / description |
strings | group resolves in groups. The description names what the probe PROVES. |
url |
https URL | The probed endpoint. |
request.method |
GET | POST, optional |
Absent: the plain GET. A deep-route probe carries the method + the same-site headers a user agent would send. |
request.headers |
string map, optional | Literal headers (content-type, origin, ...). |
request.secretHeaders[] |
{name, header}, optional |
name = the env var NAME (UPPER_SNAKE_CASE); header = the header it rides. A value key here is REFUSED — secret values never live in config. An unset var omits the header (the honest degradation, never a probe failure). |
request.body |
string, optional | What a user agent would post (e.g. a known-nonexistent credential). |
assert |
mapping, required | The content assertion; see below. |
warnLatencyMs |
positive integer | A passing probe slower than this marks the service degraded. |
slo.availability30d |
fraction in (0, 1] | The 30-day availability target (0.999 = 99.9%). |
slo.latencyP95Ms |
positive integer | The 30-day p95 latency objective. |
The assertion types (the registry lives in src/assertions.ts;
adding a spec there — the loader and the probe consume
it, no other engine change):
assert.type |
Fields | Proves |
|---|---|---|
json-equals |
path (dotted), value (a STRING — quote it in YAML) |
The body parses as JSON and the path equals the value. |
body-includes |
needles[] (non-empty) |
The body carries every needle. |
route-answers |
none | The EXERCISED deep-route verdict: any answered sub-500 response within the 10s deadline is up (the healthy answer to a probe posting a known-nonexistent credential IS a fast 401); a 5xx or the timeout is DOWN. Requires a request posture — never on a shallow GET. |
mail — the declarative half of the mailer (optional)
| Field | Type | Rule |
|---|---|---|
from |
address, required when mail is present |
The From address for both real postures. |
provider_url |
https URL, optional | A Resend-shaped HTTPS provider; the KEY is the MAIL_PROVIDER_KEY Worker secret, never here. |
The runtime postures stay env-resolved (src/mail.ts): the
send_email binding (a wrangler.toml concern), else the provider URL
- key, else the honest console no-op with the subscribe surface closed
(503, nothing stored). The
EMAIL_FROM/MAIL_PROVIDER_URLvars override this block for dev and tests.
webhooks — the machines' subscription (optional)
A list of receivers that hear about every published incident (both sources; drafts never):
| Field | Type | Rule |
|---|---|---|
url |
https URL, required | The POST target. A Slack/Teams incoming webhook URL works as-is — the payload is JSON. |
secret |
env var NAME, optional | UPPER_SNAKE_CASE; names a Worker secret. Set and resolved → deliveries carry X-Status-Signature: sha256=<hmac-sha256(body)>. A secret VALUE here is refused. |
The payload: {event: "incident.published", instance: <base_url>, incident: <the Incident>}. A 2xx delivers once; failures retry on
later rounds up to 10 attempts, then the receiver is abandoned and
named in the log.
incident_templates — the composer prefills (optional)
Up to 10 templates, each {name, kind?, title?, impact?, update_status?, update_body?, body?} (prose only; the standing
secret-refusing rules apply). The admin renders one prefill link per
template (/admin?template=<name>); nothing is forced — the operator
edits before publishing.
probe_interval_ms — optional, default 60000
The probe round interval the cron is configured for (informational;
the cron itself lives in wrangler.toml; the shell's honesty footer
renders it).
The error catalog
Every error names the file, the key path, and the problem, e.g.:
config/status.yaml: services[3].assert.type: unknown assert type 'json-contains' (known: json-equals, body-includes, route-answers, dns-answers, smtp-dialogue, canary-received)
config/status.yaml: services[3].group: unknown group 'Gamma' (declared groups: Alpha)
config/status.yaml: vantages.list: expected exactly one kind: home vantage, found 0
config/status.yaml: services[0].request.secretHeaders[0]: a secret VALUE never lives in config: name the secret (name: STATUS_PROBE_TOKEN) plus the header it rides (header: x-ribose-probe); the Worker resolves the value from its bindings at run time
config/status.yaml: services[1].assert.value: expected a string (quote it in YAML: the probe compares the JSON value as text)
test/load-config.test.ts carries a leg for every path; run it after
touching the loader or a spec.
The service display flags (TODO 77)
Two optional boolean keys on any service — on or absent, never a truthy string:
only_show_if_degraded: true— when the service is HEALTHY it folds into its group's count line on the board ("N services operational", one disclosure deep). A degraded or down service NEVER folds: the flag is display-only, the API, the feeds, the latency drill-down, and the SLA surfaces always carry the service in full.external: true— the probe watches an upstream this estate does not operate; the card carries the chip and names the posture.
Recipe: watching your own platform (the self-platform rows)
The estate watches its own supply chain like any other surface — pure
config rows, no code: probe the board itself (the home page with a
content assertion), /docs, /badge.svg, and the control plane's
URL where one runs. The honest closed posture is probeable too: the
plane's fail-closed 404 under a CONTROL_TOKEN-bearing request rides
the request posture + route-answers exactly like the identity
sign-in leg.
Recipe: DNS checks as pure configuration
DNS monitoring needs no engine change — DNS-over-HTTPS is a fetch
with a JSON answer. The first-class form is the dns-answers
assertion (TODO 72 tier A): NOERROR is the floor, and every expected
fragment must ride at least one answer's data (matched
case-insensitively, whitespace collapsed — a trailing dot or an RR
priority never breaks the match):
- id: dns-mx
name: "DNS: example.org MX"
group: DNS
description: The mail exchangers resolve and carry the expected host.
url: https://dns.google/resolve?name=example.org&type=MX
assert:
type: dns-answers
expect:
- mail.example.org
warnLatencyMs: 1500
slo: { availability30d: 0.999, latencyP95Ms: 1500 }
(The url can equally be cloudflare-dns.com/dns-query?... — then the
request posture must carry accept: application/dns-json, as in the
json-equals form below.) The mail-path recipes ride this row:
type=MX for the exchangers, type=TXT on the apex for SPF, on
selector._domainkey for DKIM, on _dmarc for the policy.
The raw form (what dns-answers wraps, still valid):
- id: dns-www-a
name: "DNS: www A record"
group: DNS
description: Resolves the A record via DoH and asserts the answer's rcode.
url: https://cloudflare-dns.com/dns-query?name=www.example.org&type=A
request: { method: GET, headers: { accept: application/dns-json } }
assert: { type: json-equals, path: Status, value: "0" }
warnLatencyMs: 1500
slo: { availability30d: 0.999, latencyP95Ms: 1500 }
Assert Status: 0 (NOERROR) for resolution health, or a known
Answer.1.data address for a pin. Cert expiry needs no recipe here:
the CP posture refuses vendor-fetched cert data; a dedicated probe
would be a new assertion spec (the registry is the extension point).
Recipe: the round-trip canary (delivery measured, TODO 79 tier C)
The mail block's canary turns email monitoring from inference into
measurement: the prober sends a tokened mail to the address, the
domain's Email Routing hands it to the worker's email handler, the
arrival is stamped, and the canary service's probe asserts the token
arrived within the SLO:
mail:
from: notify@example.org
canary: { address: canary@example.org, slo_ms: 300000, cadence_ms: 600000 }
services:
- id: mail-canary
name: "Mail: delivery"
group: Mail
description: A canary mail completes the round trip within the SLO.
url: https://mail.example.org/
assert: { type: canary-received }
warnLatencyMs: 60000
slo: { availability30d: 0.999, latencyP95Ms: 120000 }
slo_ms(default 300000): an outstanding flight older than this is DOWN, named with the age.cadence_ms(default 600000): one measurement per cadence, not per probe round — the mail volume is yours to dial.- The receipt matches the OUTSTANDING token only (a forged or stale subject writes nothing); the flight is outstanding from the WRITE, so a crash mid-send is honestly overdue.
- An unset or non-working mailer reads DOWN with the reason — never a quiet green. Home vantage only: mail is the home posture; the relays never send and never write a row.
- Go-live act: the domain's Email Routing routes the canary address
to the worker (the wrangler
emailhandler ships in src/index.ts).
Recipe: the SMTP transport (the exercised mail path)
The mail path can be EXERCISED, not inferred: the smtp block makes
the probe run the real dialogue — banner, EHLO, (STARTTLS,) QUIT —
and the dialogue IS the verdict (assert.type: smtp-dialogue). The
service's url stays required: it is the page a human checks (the
webmail or the provider's surface), never fetched for this service:
- id: mail-path
name: "Mail: submission path"
group: Mail
description: The submission host answers, offers TLS, and speaks ESMTP.
url: https://mail.example.org/
smtp: { host: mail.example.org, port: 587, mode: starttls } # 465 → mode: implicit
assert: { type: smtp-dialogue }
warnLatencyMs: 2500
slo: { availability30d: 0.999, latencyP95Ms: 2000 }
A 5xx banner, a refused EHLO, a failed STARTTLS, a hang past the deadline — each is DOWN with the code in the reason; the dialogue is bounded (10 s, an 8 KiB reply cap) and never sends a payload (no message is composed or delivered — tier C's canary is a separate, owner-gated act). Platform note: outbound port 25 is refused by the cloud; 465/587 are the supported postures (docs/manual/09).
The fixtures (config/fixtures/)
minimal.yaml— two services across two groups, one json-equals + one body-includes, home vantage only, no mail. The proof the schema is not estate-shaped; the engine legs run against this shape.relay.yaml— the minimal shape plus the declaredrelay-1vantage: what the multi-vantage verdict matrix drives.mail.yaml— the minimal shape plus the provider mail posture (no real key).
Hostnames are RFC 2606 .test: nothing resolves, and no test ever
fetches them. The suite proves config-independence by passing with
CONFIG=config/fixtures/minimal.yaml npm test.
Migrating an estate that still carries TypeScript registries
scripts/convert-registries.ts turns config/services.ts +
config/vantages.ts into the YAML mechanically (data verbatim;
secretHeaders record → {name, header} list) and validates its own
output through the loader. Doctrine prose does not survive a
mechanical pass: port the registries' comments by hand afterwards, and
add the mail block by hand.
npx tsx scripts/convert-registries.ts \
--services <checkout>/config/services.ts \
--vantages <checkout>/config/vantages.ts \
--name "Estate Status" --base-url https://status.example.org \
--description "..." \
--logo-light https://.../logo.svg --logo-dark https://.../logo-white.svg \
--link "Estate|https://www.example.org/" \
config/status.yaml