Skip to content
GitHub
Infrastructure

Hosting

Spectral runs its API/workers runtime on a single Cloudflare app container and its human-facing surfaces on Cloudflare Pages, all against a locked Supabase substrate behind the Cloudflare edge. The workload is I/O-bound end to end — the only heavy compute is LLM inference, which runs at the provider on the authoring path, never on the synchronous decision path. Decision lineage in ADR-109 (hosting topology) and ADR-110 (provisioning); operational detail in docs/runbooks/hosting.md.

  • Compute — one Cloudflare Container for API/workers. Built from infra/docker/app.Dockerfile and launched by app_supervisor.py, the container runs both entrypoints as sibling processes: the FastAPI API (port 8000) and the background workers runtime. A thin Cloudflare Worker fronts the container and proxies every request to the API port. The predicate/decision tier executes in-process inside the container for alpha — the sandbox (ADR-083) already bounds the blast radius. The API and workers stay separately-launchable entrypoints, so splitting a tier onto its own host later is a deploy-manifest change, not a rewrite.
  • Human-facing surfaces — Cloudflare Pages. app., ops., docs., and codex. are separate Pages projects. Dashboard and Operations are Vite SPA builds; Pages Functions provide same-origin API proxying, and Operations/Codex carry JWKS-local staff gates.
  • Edge — Cloudflare. Authoritative DNS, edge TLS, every product hostname proxied (including api.), per-key rate limiting + WAF posture, the codex Pages-Function JWKS gate, and cookie discipline, per ADR-052.
  • Data / event substrate (locked) — Supabase. Managed Postgres + Auth + pgvector + Realtime. The source of truth for core.outbox, generation stamping, LISTEN/NOTIFY, and core.event_handled per ADR-044. The container and the deploy’s migration step both connect over the session pooler (session mode supports LISTEN/NOTIFY, so the outbox consumer keeps its listen path). The substrate does not move.
HostnameAudienceEdge posture
app.runspectral.comCustomer dashboard (spectral-dashboard Pages)Proxied edge + /api/* proxy Function
ops.runspectral.comStaff Operations app (spectral-operations Pages)Proxied edge + JWKS gate + /operator/* proxy Function
api.runspectral.comProgrammaticProxied edge
docs.runspectral.comPublic docsProxied edge
codex.runspectral.comStaff Codex (JWKS-gated)Proxied edge + Pages Function

Cookie scope: Domain=runspectral.com eTLD+1 — cross-subdomain session sharing.

Disaster recovery is Supabase-native managed backups + point-in-time recovery (ADR-040), adopted before customer data lands. There is no self-run backup pipeline, no nightly-dump cron, and no separate backup bucket — the managed backup tree is isolated from the application runtime by the provider, and PITR covers the recovery window.

Two classes of runtime configuration, placed differently:

  • Rotating key material — Supabase keys, provider API tokens, OAuth secrets — is resolved by tools/provision/provision.sh from infra/environments.toml + 1Password into the GitHub Environment (ADR-110), and applied to the container or Pages deploy at deploy time.
  • Code-coupled valuesSPECTRAL_GENERATION, handler concurrency limits — are set as per-service container vars at deploy time, never in the shared environment. Correctness depends on the running image, so the value moves with the image.

One shared container today. The separately-launchable entrypoints are the re-split seam; the documented triggers (ADR-109):

  • Supabase-locality latency materially degrades the closed loop.
  • A Cloudflare control-plane outage exceeds tolerance.
  • Cost exceeds the modeled envelope at dogfood scale.

The mitigation is the same seam: re-split the DB-chatty tier onto its own host via the deploy manifest, no code rewrite.