Accepted
ADR 0053 — Headless API explorer
A built-in, read-only, header-authenticated page that browses the headless API live without putting a token in a URL
Context
The headless API at /api/v1 authenticates every route with a bearer token (ADR 0023). That makes it impossible to demonstrate through a plain hyperlink: a browser following an anchor sends no Authorization header, so a link straight to /api/v1/_meta returns 401 UNAUTHENTICATED. We wanted a way to browse the API live without weakening the auth model, and without ever putting a token in a URL.
Decision
Cusp serves a self-contained HTML explorer at GET /api/v1/_explorer. The page calls the API from client JavaScript with the token in the Authorization header — never in the URL, so the token never lands in server logs, browser history, bookmarks, or the Referer header. It is read-only by construction: the UI only issues GETs against _meta, a collection listing, and a single document by slug, and never renders a create, update, or delete control, so it cannot mutate even when handed a write-scoped token.
The route is mounted outside the headless bearer gate (the same posture as the public search endpoint, ADR 0045) so a visitor can reach the page without a token. The API calls the page then makes are still authenticated and rate-limited by the headless surface. The explorer page itself is per-IP rate-limited (60 requests per minute) and carries a strict Content-Security-Policy: default-src 'self', with the inline script and style admitted by a per-response nonce, and connect-src 'self' so the page can only call its own origin.
Safe by default; pre-wired on showcase deployments
Because the docs site runs the same Cusp Worker codebase as every deployment, the explorer is a core feature, not a docs-only page — so it must be safe by default. With no configuration the page shows a token field (persisted only in the browser tab's sessionStorage) and exposes nothing. A deployer whose content is already public can pre-wire a token for a zero-friction live demo by setting the EXPLORER_PUBLIC_TOKEN Wrangler secret.
The route bakes that token into the page only after verifying it against api_tokens and confirming it is strictly read-only: every scope must match {slug|*}:read. Write scopes, the full-admin * scope, and — critically — read-drafts are all rejected, so a public explorer can never surface unpublished content. A misconfigured token is not baked; the page falls back to the input field and logs a one-line warning. The token lives as a Wrangler secret rather than a settings row so it stays out of D1, out of entity-version snapshots, and out of backup exports.
Alternatives considered
- Anonymous read mode on /api/v1 — let GETs work with no token. Rejected: it is a new public read surface that every deployment inherits, against the default-deny posture, and a bigger change than the showcase needs.
- A token in a query parameter (?token=) so a link could authenticate. Rejected: tokens in URLs leak into logs, history, bookmarks, and the Referer header, and a write-scoped token in a URL is a genuine footgun. The header carrier keeps the credential out of the URL entirely.
- Point the docs footer link at the reference document instead. Kept as the fallback if the explorer is undesired, but it does not show the API working.
Consequences
- Every deployment gains a built-in, read-only API explorer an admin can use by pasting a token — useful DX beyond the docs showcase.
- The auth model is untouched: the header stays the only token carrier, and the explorer is read-only and CSP-locked.
- Showcase deployments (docs, demo) load live data anonymously once EXPLORER_PUBLIC_TOKEN is set to a *:read token; drafts can never be exposed through it.