ADR-0120 — Status Page Hosting: where does /api/status/public live?
Status: Accepted Date: 2026-04-28 UTC Refs: #601, #581, status-raxx-app.md
Context
status.raxx.app requires a backend that serves /api/status/public — the JSON feed consumed by the public status page React app. Three candidate homes were evaluated:
- Cohabit with
raxx-api-prodHeroku dyno — the existingbackend_v2/Flask app already has a/api/status/namespace for operator use. Adding a/api/status/publicroute is one blueprint addition. - Dedicated Heroku app
raxx-status-prod— a new Heroku dyno running a thin Flask subset, isolated from the trading API. - Cloudflare Worker — a Worker that polls the Heroku API (or an internal endpoint) and serves public status JSON at the edge, globally cached.
The public endpoint is read-only and unauthenticated. It does not execute trades, access PII, or write any state. Its primary quality requirement is availability: the status page must be reachable even when the trading API is degraded.
Decision
Cloudflare Worker for the public-facing /api/status/public JSON endpoint.
The status page is the surface users check when something is wrong. Cohosting the status endpoint on the dyno being monitored creates a availability paradox: if raxx-api-prod is down, the status endpoint reporting it is also down. A Worker breaks that dependency — it runs at the Cloudflare edge, has no shared blast radius with Heroku, and its global PoP deployment means read latency is low without any additional caching configuration.
The Worker's data source is a Postgres-backed state table (see status-raxx-app.md §3) that the polling worker writes to. The Worker reads this table via a connection-pooled query or via a lightweight internal Flask endpoint on raxx-api-prod that the Worker calls with an internal API key. The Worker response is cached at the edge with Cache-Control: public, max-age=60 for the surfaces list and max-age=300 for the incidents log.
Internal operator endpoints (/api/status/ — full probe data, audit log, FreeScout webhook receiver) remain on raxx-api-prod. The Worker only serves the projection defined in the public API contract.
Consequences
Positive:
- Availability boundary: public status endpoint is decoupled from the Heroku dyno's health. CF network uptime (99.99%+) is independent of Heroku uptime.
- Free tier capacity: Cloudflare Workers free tier (100k req/day) covers current and projected public traffic comfortably without incremental cost.
- Edge caching: 60-second max-age means the Worker rarely calls the upstream data source. Cache invalidation on state change is possible via CF Cache API from the webhook receiver.
- No new Heroku dyno or ops overhead.
Negative:
- New deployment unit: the Worker is a separate deployable from backend_v2/. Feature-developer needs a wrangler.toml and a workers/status-public/ directory. This is a modest ops addition.
- Internal API key required: the Worker must authenticate to its data source. This key lives in CF Worker environment secrets (not in code). It must be rotatable without Worker redeployment (CF secrets are set via Wrangler and take effect on next request without redeploy).
- Worker cold-start latency is negligible for V8 isolates but relevant if the Worker is paused due to zero traffic (never a problem for a public status page with organic traffic).
Alternatives Rejected:
- Cohabit on
raxx-api-prod: Rejected because the blast radius is shared. A Heroku dyno restart, a memory ceiling breach, or a bad deploy puts the status endpoint offline at the moment it is most needed. - Dedicated Heroku app: Rejected because it adds recurring dyno cost and operational complexity (another app to maintain, another Heroku config to keep in sync) without solving the availability problem — it is still a Heroku-hosted process subject to Heroku's own incidents.
Implementation notes for feature-developer
- Worker source lives in
workers/status-public/at repo root. - Worker calls
GET /internal/status/snapshotonraxx-api-prodwith headerX-Status-Worker-Key: <secret>. That secret is in Infisical at/raxx/status-worker/internal-keyand in CF Worker secrets asSTATUS_WORKER_INTERNAL_KEY. - Cache invalidation:
POST /internal/status/invalidateon the Worker (CF Cache API purge) is called by the FreeScout webhook receiver after a state write — ensuring the status page reflects ticket changes within the 5-minute SLA. - CORS: Worker must emit
Access-Control-Allow-Origin: https://status.raxx.appon all responses.