Conormis status

The manual

The HTTP surface

Every route the deployed service answers. The dispatch is a route table in src/worker.ts (adding a surface = adding a row); the postures below are the contract. Base: the instance's site.base_url (production: https://status.ribose.com).

The pages

Path What
/ The board: the worst-of banner, per-service cards with the per-vantage split, the scheduled-maintenance section, the month-grouped incident history, the SLO block, the subscribe form. Cache: 30 s.
/history The Statuspage-era URL: the month-grouped timeline of every published incident (ended maintenance folded in). Upcoming maintenance stays on /. Cache: 30 s.
/latency/<serviceId> The latency drill-down: one SVG line per declared vantage over the 24-hour ring + the per-window p95 table. No JavaScript. Unknown id: 404. Cache: 30 s.
/health { "status": "ok" } — the worker's own liveness, nothing more (never a health claim about the estate).

The feeds

Path What
/feed.xml RSS 2.0 of the published incidents (drafts never ride).
/feed.atom Atom 1.0 of the same; <updated> carries the latest update's timestamp.
/history.rss, /history.atom The Statuspage-era aliases of the same feeds.

Every feed path takes ?service=<id> (the per-component pattern): the entries naming that service plus the estate-wide rows (no services). An unknown id answers 404. Maintenance items carry the Maintenance: title prefix. Cache: 300 s.

The machine surface

GET /api/summary.json

no-store. The whole board, machine-readable:

{ generatedAt, prober: { lastRunAt },
  vantages: [ { id, kind, label, description, reporting,
                lastReportAt, colo } ],      // home first; colo observed, never claimed
  services: [ { id, name, url, state,        // state = the worst-of verdict
                reason, lastGoodAt,
                uptime30d, uptime90d,        // truncated strings, or null = no data
                vantages: [ { id, label, kind, reporting, state, reason,
                              lastProbeAt, lastGoodAt, lastReportAt, colo } ] } ] }

The per-vantage split is the truth; the top-level state is the worst-of of those, never a merged green.

GET /badge.svg

The embeddable badge: the instance's overall verdict (the worst-of across every service), or one service's with ?service=<id> (unknown id: 404). The SAME verdict engine as the board; no data renders gray, never green. Cache 30 s.

Webhook deliveries (outbound)

Each declared receiver (the webhooks: block) is POSTed {event: "incident.published", instance, incident} on every published incident — once per receiver on a 2xx, retried on later rounds up to 10 attempts, then abandoned and named in the log. The optional declared secret signs the body as X-Status-Signature: sha256=<hmac-sha256(body)>. Slack/Teams incoming-webhook URLs work as receivers unmodified.

POST /api/vantage-report

The relay's only way in (a relay holds no KV binding). Postures, fail-closed in every direction:

The subscribe flow

The operator surface

The write API (the machines' operate surface)

Bearer OPS_TOKEN on every call (unset: the whole surface answers 404) — OR, with an org declared, one of the org's named API keys (a role-scoped Bearer created on the admin's org card, shown once, hash-kept; publish level for either role). JSON in, JSON out; the SAME validation model the admin forms use — the error strings are the actionable ones. This is deliberate even when admin.oidc moves the human sign-in to the identity service: machines have no browser, so the write API never speaks OIDC.

Call Body Returns
POST /api/incidents the composer's JSON shape (title, date?, kind?, start, end?, impact, service: [ids], update_status?, update_body?, body?) 201 {incident}
POST /api/incidents/<slug>/updates {status, body} (the live trail) 200 {incident}
PUT /api/incidents/<slug>/body {body} (the postmortem act: set/replace the write-up on a RUNTIME incident; build-time rows are git's) 200 {incident}
DELETE /api/incidents/<slug> — 200 {ok, deleted}
POST /api/subscribers/delete {email} (the GDPR delete) 200 {ok, deleted}

The off-platform heartbeat

scripts/heartbeat.ts (via .github/workflows/heartbeat.yml, every 30 minutes): the TRUE external probe of this page. Healthy only when the board answers with its identity AND /api/summary.json parses with a prober heartbeat within ten probe intervals. On failure it opens (or comments on) the one canonical heartbeat-labeled tracking issue; on recovery it closes it. npm run heartbeat runs it locally; it exits nonzero and writes heartbeat-report.md.

The KV layout (for the curious operator)

svc:<id> (the home vantage's row: current probe, 24 h ring, 90 day rollups), vantage:<id>/svc:<id> + vantage:<id>/meta (the relays', written only through the report surface), prober:meta (the heartbeat), draft:<id> (the automation's slots), sub:<token> + subx:<email-hash> (subscriptions + index), notify:state (the one-mail-per-incident ledger), rt-incident:<slug> (the runtime incidents authored through the admin).

The brand assets (TODO 78)

The control plane (provisioning v1)

A separate Worker (ribose-status-control, wrangler.control.toml) owns the SaaS registry: orgs, pages, bindings. Bearer CONTROL_TOKEN; unset, the whole surface answers 404. It never deploys — it emits reviewable instance YAML through the one emission engine and records where the emission went. POST /v1/pages/:slug/op-client additionally registers the page's admin SSO client at the plane's identity issuer (RFC 7591; gated on the OP_ISSUER var + the OP_DCR_TOKEN secret — 404 without either) and re-emits the YAML with admin.oidc real. The full lifecycle and routes: docs/manual/06-control-plane.md.

The page registry (shared deployments)

/api/summary.json carries the page-level indicator (TODO 77) as status: {indicator, description} — the same derivation the gauge and the overall badge render: operational | degraded | partial | major | maintenance | unknown, the description from the strings contract's page.* words. /badge.svg without ?service= renders the same indicator.

Every page surface also serves under its path mount /p/<slug>/… (board, feeds, badge, summary, subscribe, reports) — the page is reachable before its custom domain lands. The full runbook (isolation, custom-domain hosts, quotas): docs/manual/07-shared-deployments.md.

The embed surfaces + the reports