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:
- Unset
STATUS_RELAY_TOKENon the worker: 404, the surface does not exist. - Wrong/missing bearer: 401. Not POST: 405.
- Envelope:
{ vantage, colo?, roundAt, results: [{ serviceId, result }] }. An undeclared vantage (includinghome) refuses the whole report: 403.roundAtoutside ±24 h: 400. - Per entry (a relay running newer code than the board degrades
honestly, entry by entry): unknown
serviceId, a timestamp outside ±(10 min past / 5 min future), a non-booleanok, a non-statushttp, a non-boundedlatencyMs— that entry is dropped and named inignored; the rest lands asvantage:<id>/svc:<serviceId>rows plus the vantage meta. Success:{ ok, vantage, accepted, ignored }.
The subscribe flow
POST /subscribe— form fields:email(required) and any number ofservicecheckboxes (the registry's ids; none = every service; unknown ids are ignored). Without a mailer configured: an honest 503, nothing stored. The answer is generic either way — the endpoint is never an enumeration oracle.GET /subscribe/verify?token=…— the capability link that confirms (idempotent; unknown token 404).GET /unsubscribe?token=…— DELETES the address, the token, and the index row. Unknown token 404.
The operator surface
GET /ops/drafts— BearerOPS_TOKEN(unset: 404, the surface does not exist). The automation's draft-incident review bundle: each draft with its evidence, the ready-to-pasteincidents/row (draft: true), and the suggested filename. Drafts never render, never feed, never mail — humans publish.- The admin interface (the SaaS path;
admin: falsein the instance YAML closes it, and an unsetOPS_TOKENcloses everything):GET /admin— the login page; with a valid session, the dashboard: the open drafts (each with its publish form — the automation's evidence prefilled, the copy editable), the incident composer (title, kind, window, affected services, impact, the opening update, the write-up), and the runtime incidents (append an update on the live trail; delete).POST /admin/login(formtoken) — issues the session: an HMAC-signed, HttpOnly, Secure, SameSite=Strict cookie (TTL fromadmin.session_ttl_hours, default 8, keyed by the OPS token). Wrong token: the login page with the error.POST /admin/logoutclears (and deletes the server-side session row — sessions are revocable, never just signed).GET /admin/oidc/login+GET /admin/oidc/callback— the identity-service posture (admin.oidcconfigured): the login page offers the identity button INSTEAD of the token form; the flow is authorization-code + PKCE S256 against the discovered endpoints, and a verified ID token whosesubis listed or whoseadmins_claimis truthy issues the SAME session. Withoutadmin.oidcboth routes 404. The setup act:docs/manual/03-admin-guide.md.POST /admin/incidents(composer) ·POST /admin/incidents/<slug>/updates·POST /admin/incidents/<slug>/delete·POST /admin/drafts/<serviceId>/publish·POST /admin/drafts/<serviceId>/delete(the dismiss override; a still-failing service re-drafts) ·POST /admin/subscribers/delete(formemail; removes the address's rows entirely) — session-gated form POSTs validating exactly what the build-time generator validates.- The org model (
orgdeclared in the instance YAML): the routes above split by role — operators keep the publish surfaces; owners additionally getPOST /admin/org/invites(formemail,role) ·POST /admin/org/invites/delete(formemail) ·POST /admin/org/members/delete(formsub; the config floor is refused with the reason) ·POST /admin/org/pages(formsslug,title,url) ·POST /admin/org/pages/delete(formslug). Owner-only acts answer 403 for operators. Membership resolves per request. Owners additionally manage the org's named API keys (POST /admin/org/keysformsname,role— the key renders ONCE on a 201 page;POST /admin/org/keys/deleteformhash) and the page-provisioning emission (POST /admin/org/provision: formsslug,title,base_url,description,services_text— one service per lineid | name | url | needle; answers the starter YAML, the worker manifest, and the operator checklist; writes nothing). - Backups:
GET /admin/backupdownloads the versioned state document (session OR the OPS_TOKEN bearer — automation-friendly);POST /admin/restore(multipartbackupfile) validates every key against the known state prefixes and MERGES (same-key rows overwrite, nothing deleted). See the manual's backups chapter. - Runtime incidents live in KV (
rt-incident:<slug>), the SAME Incident model as the build-time rows; every consumer — the board,/history, all four feeds, the notify leg — renders the MERGED set. Publishing a draft does not stop the automation (its slot stays; a still-failing service re-drafts per its own rules). - Build-time rows (
incidents/*.md) remain the git-PR publish path; both paths coexist, and drafts never render from either.
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)
GET /brand/logo-lightandGET /brand/logo-dark— the admin's uploaded logos (the Design register), served page-scoped with the exact content type,nosniff, a short public cache, and — for SVGs — the sandbox CSP that keeps a user-uploaded vector from ever executing in the site's origin. Unset, 404: the render falls back to the config's declaration.
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)
GET /api/pages— Bearer STATUS_RELAY_TOKEN. Every active page's probe spec (slug, baseUrl, relays, services) — the stateless relays' per-round listing.POST /api/pages— Bearer OPS_TOKEN. Body{slug, host?, yaml}: the instance YAML through the one strict loader; 400 with the loader's error when it does not parse. Re-registering bumps the revision.DELETE /api/pages— Bearer OPS_TOKEN. Body{slug}: retirement (a state, never a delete — the rows stay for the backup posture).- The vantage report (
/api/vantage-report) carries an optionalpagefield (the registry slug; absent = the deployment's own page); a report for an unknown or retired page writes nothing.
/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
GET /embed— the compact board (its own minimal document, no chrome, noindex): the worst-of word with its colored edge and one lamp row per service. Page-scoped by Host like every public surface; the one-line widget (/widget.js, a static asset) iframes it:<script src="https://status.example.test/widget.js" data-status-host="https://status.example.test" async></script>GET /calendar.ics— the windows calendar (RFC 5545): every published incident and maintenance window as VEVENTs (drafts never); the maintenance prefix matches the feeds.GET /reports/:YYYY-MM— the monthly SLA report: the measured month against each service's declared availability, its incident count, its probe days.?format=csvexports,?format=jsonis the machine shape; the HTML page states when the month is partial. Percentages truncate; no data reads no data.