An open-source, CDN-native status page generator — a modern take on upptime.
🔗 Live demo: demo.statusbeam.dev — a StatusBeam instance monitoring a few public services, running on Cloudflare.
StatusBeam monitors your services, records their uptime as durable time-series data, and publishes a fast, good-looking status page to the edge. It keeps the parts of upptime that people love — config-as-YAML, zero servers to babysit, badges, a public JSON API — while fixing upptime's biggest structural weaknesses:
- No client-side rate limits. upptime's page calls the GitHub API from the visitor's browser (unauthenticated, 60 req/h/IP), so popular pages break with a "rate limit exceeded" screen. StatusBeam renders every byte at the edge from its own store — the browser never talks to a third-party API.
- Reliable scheduling. upptime rides GitHub Actions cron, which is best-effort (a "every 5 min" job can slip to 15–60 min). StatusBeam uses Cloudflare Cron Triggers, which fire on time.
- A store that scales. upptime treats git commit history as its database and walks it through a rate-limited API. StatusBeam uses Cloudflare D1 + KV, purpose-built for time-series reads.
- A current, maintained frontend. upptime's page is built on Svelte 3 + Sapper, both end-of-life. StatusBeam is Astro + shadcn/ui.
🚧 Active development. The core pipeline is live end-to-end — HTTP checks on Cloudflare Cron write to D1/KV, the Astro page renders 90-day uptime bars, response-time charts, and an incident timeline at the edge, and status changes fan out to Slack/webhooks while purging the edge cache. A live demo runs on Cloudflare. TCP/SSL checks, a public API + badges, and more notification channels are next — see the Roadmap.
StatusBeam is deliberately split into three independent layers. Each can be understood, deployed, and replaced on its own.
┌──────────────────────────────────────────────────────────────┐
│ 1. CHECK LAYER — Cloudflare Cron Worker │
│ • Cron Triggers ping every configured service on schedule │
│ • Derives up / degraded / down from status + response time │
│ • Writes time-series to D1, current snapshot to KV │
│ • On a status change: enqueue a notification event, and │
│ purge the page/badge cache by tag (ctx.cache.purge) │
└──────────┬────────────────┬─────────────────────┬────────────-┘
│ writes │ enqueues │ purges on change
▼ ▼ │
┌───────────────────┐ ┌──────────────────────┐ │
│ D1 (time-series, │ │ 2. NOTIFY LAYER — │ │
│ incidents) │ │ Queue consumer │ │
│ KV (current │ │ • Email, Slack, │ │
│ snapshot) │ │ webhook, RSS/Atom │ │
└─────────┬─────────┘ └──────────────────────┘ │
│ reads at the edge │
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ 3. DISPLAY LAYER — Astro site on Cloudflare │
│ • Renders the page at the edge from D1/KV (no browser → │
│ third-party API calls, so no client rate limits) │
│ • Fronted by Workers Cache (tiered edge cache): renders │
│ set Cache-Control + stale-while-revalidate, hits skip │
│ the Worker + D1, concurrent requests collapse; the check │
│ layer purges by tag on change, so updates are near- │
│ instant, not TTL-bound │
│ • shadcn/ui via React islands for the interactive bits │
│ (charts, time-range filters); everything else ships 0 JS │
│ • Emits shields.io-compatible badge JSON + a public API │
└──────────────────────────────────────────────────────────────┘
Because the check and display layers live on Cloudflare — not on the infrastructure being monitored — your status page stays up even when your own services are down. That resilience is the whole point of a status page.
| Layer | Choice | Why |
|---|---|---|
| Frontend | Astro 7 | Static-first (ideal for a mostly-read page), ~0 KB JS by default, a Cloudflare first-party framework (acquired Jan 2026) with workerd dev/prod parity, a Rust compiler (15–61% faster builds), and a stable Astro.cache route-caching API plus an experimental cacheCloudflare() provider for Workers Cache. |
| UI components | shadcn/ui (React islands) + Tailwind CSS | Copy-in-your-repo components you own and can fork — perfect for OSS. Used natively via Astro's React islands; hydrated only where interactivity is needed. |
| Charts | shadcn/ui charts (Recharts) | Response-time graphs, themed and dark-mode-ready out of the box. |
| Check scheduler | Cloudflare Cron Triggers (Worker) | On-time execution, unlike GitHub Actions cron. |
| Data store | Cloudflare D1 (SQLite) + KV | D1 for time-series & incident history; KV for the current snapshot. |
| Edge cache | Cloudflare Workers Cache | Tiered cache in front of the Astro Worker: Cache-Control + stale-while-revalidate, request collapsing, and tag-based purge on status change — near-instant updates without hammering D1. Today via Cache-Control headers; Astro 7's Astro.cache / cacheCloudflare() is the forward path. |
| Notifications | Cloudflare Workers + Queues | Email / Slack / webhook / RSS on status change, decoupled from the UI. |
| Deploy target | Cloudflare (primary) · Vercel (supported) | Astro adapters target both; Cloudflare is the native, batteries-included path. |
| Tooling | Bun · Wrangler · TypeScript | Bun for install/scripts; Wrangler for Worker + D1 + KV. |
The full rationale — including why Astro over SvelteKit and TanStack Start, and why
a Cron Worker over GitHub Actions — is in
docs/adr/0001-tech-stack.md.
The UI follows the information architecture proven by Statuspage.io and the modern, static-first aesthetic of Instatus:
- Overall-status banner — one calm, unambiguous line ("All Systems Operational") in a single color that rolls up the worst component state.
- Component rows — one per service, grouped and collapsible, each with a status pill: Operational / Degraded / Partial Outage / Major Outage / Maintenance.
- 90-day uptime bars — the signature timeline: one colored bar per day, hover for date + uptime % + linked incidents, gray for no-data days. Adaptive intervals (Instatus-style) let the same component render other windows.
- Incident timeline — a reverse-chronological, date-grouped feed; each incident threads timestamped updates through the Investigating → Identified → Monitoring → Resolved lifecycle. Scheduled maintenance is a distinct, forward-looking entry.
- One severity token system — five states as CSS variables (light + dark, OKLCH), driving the banner, pills, and bars from a single source of truth. Dark mode ships by default. Color is paired with icon + text for accessibility.
A Bun-workspaces monorepo. The three runtime layers map to three workspaces, with
the domain logic shared in core:
statusbeam/
├── apps/
│ ├── web/ # Astro status page (Cloudflare adapter + Workers Cache)
│ └── worker/ # Cron Worker: checks + notifications (D1/KV, schema.sql)
├── packages/
│ └── core/ # shared config schema (zod), types, status derivation
├── status.config.example.yml
├── mise.toml # pinned toolchain (node, bun)
└── orca.yaml # worktree setup
Local development:
mise install # pinned node + bun
bun install # install workspaces
bun run test # core unit tests (bun:test)
bun run dev # Astro dev server (renders sample data without bindings)A single YAML file is the only thing you edit — the same idea as upptime's
.upptimerc.yml. Copy status.config.example.yml to
status.config.yml:
# status.config.yml
name: Acme Status
sites:
- name: Website
url: https://example.com
check: http # http | tcp | ssl | statuspage | incidentio
expectedStatusCodes: [200]
maxResponseTime: 2000 # ms → "degraded" above this
- name: API
url: https://api.example.com/health
check: http
- name: Claude # mirror an Atlassian Statuspage (status.claude.com, *.statuspage.io, …)
url: https://status.claude.com # base URL; /api/v2/summary.json is appended for you
check: statuspage
- name: Claude API # or track one service on that page by component name/id
url: https://status.claude.com
check: statuspage
component: Claude API (api.anthropic.com)
- name: OpenAI # incident.io status pages read the same way (Statuspage-compatible)
url: https://status.openai.com
check: incidentio
notifications: # all optional; keep the real Slack URL (a secret) in your KV config
slack:
webhookUrl: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX
webhooks:
- url: https://example.com/status-hook
theme:
logoUrl: /logo.svg
darkMode: true
locale: en # fallback UI language: en | zh | ja | ko (default en)Each site sets a check kind:
check |
What it does |
|---|---|
http |
Fetches url; up/degraded/down from the status code and response time. |
tcp |
Reserved — currently falls through to http (roadmap). |
ssl |
Reserved — currently falls through to http (roadmap). |
statuspage |
Mirrors an Atlassian Statuspage's own verdict. See the Statuspage adapter guide. |
incidentio |
Mirrors an incident.io status page (Statuspage-compatible). See the incident.io adapter guide. |
The Statuspage adapter reads a vendor's /api/v2/summary.json (Claude,
Vercel, *.statuspage.io, …) and maps their overall indicator — or a single
component you name — to a status. Full reference, status-mapping tables, and
edge behavior: docs/adapters/statuspage.md.
The incident.io adapter reads the same Statuspage-compatible
/api/v2/summary.json that incident.io status pages (status.openai.com,
status.incident.io, …) serve — so incidentio and statuspage behave
identically; use whichever names your vendor:
docs/adapters/incidentio.md.
The status page UI is translated into English (en), Simplified Chinese (zh),
Japanese (ja), and Korean (ko); dates and relative times localize
automatically.
Each language is a URL prefix — /en/, /ja/, /ko/, /zh/ — so every
language is cached independently at the edge (no cache fragmentation). Visiting
the bare / redirects to the visitor's language, chosen in this order:
- their remembered choice (a
localecookie, set when they pick a language), - their browser's
Accept-Language, - the deployment's
theme.locale(used only when the above don't match a supported language), - English.
A language switcher in the footer lets visitors change and remember their choice.
Every deployment exposes a small public JSON surface at /api/*, served from the
same edge-cached KV snapshot as the status page (and purged on the same status
changes, so badges never lag the page).
The badge routes speak the shields.io endpoint
protocol — point shields.io at one and it renders the SVG; StatusBeam only
emits the JSON. Replace <origin> with your status page's URL and <slug> with
a component's slug (the slug from status.config.yml, or the slugified name):
| Badge | Endpoint |
|---|---|
| Overall status | /api/badge.json |
| Site status | /api/badge/<slug>.json |
| Site uptime | /api/badge/<slug>/uptime.json (?period=day|week|month, default month) |
| Response time | /api/badge/<slug>/response-time.json |

Colors are derived from severity (green → operational, yellow → degraded, red →
down), uptime ratio, and response time. Add any shields.io query (?style=flat-square,
?label=API, ?logo=cloudflare) to restyle the rendered badge.
GET /api/status.json— the whole dashboard: rolled-upstatusplus a lean per-site summary (status, response time, day/week/month uptime).GET /api/status/<slug>.json— one site's full record, including the 90-day history and response-time samples.
Both send Access-Control-Allow-Origin: *, so a browser can fetch them directly.
StatusBeam deploys to any Cloudflare account (Workers + D1 + KV + Pages). Vercel is also supported for the display layer via Astro's Vercel adapter.
You deploy StatusBeam as a package, not a fork (ADR-0002):
your repo holds only your config, and the app is a versioned dependency. Scaffold a thin
project, then let the statusbeam CLI provision D1 + KV, wire your custom domain and cron,
apply the schema, upload status.config.yml, and deploy both Workers — idempotent, safe to
re-run.
bunx create-statusbeam my-status # or "Use this template" on statusbeam-template
cd my-status
bunx wrangler login # or export CLOUDFLARE_API_TOKEN
bun install
bunx statusbeam setup # provisions, configures, deploys (--skip-deploy to stop before deploy)Upgrading is bunx statusbeam update — no upstream merge. Prefer to modify the app source?
You can still fork and deploy the monorepo directly; see the appendix in
DEPLOYMENT.md.
By default the page is edge-cached for s-maxage=60, so a status change shows up
within a minute. For near-instant updates, the check Worker purges the edge
cache by Cache-Tag
the moment a status flips. The page already emits a matching Cache-Tag response
header (status-page + one status-site-<slug> per component); you just provide
the Worker two secrets (purge-by-tag is available on all Cloudflare plans since April 2025):
bunx wrangler secret put CF_API_TOKEN # API token with the "Cache Purge" permission
bunx wrangler secret put CF_ZONE_ID # the zone serving your status pageWhen these are unset the purge is skipped (logged, not fatal) and the page simply refreshes on its 60s TTL.
Full runbook — the CLI, provisioning, config, secrets, CI deploy, and the fork-from-source appendix — is in DEPLOYMENT.md.
Shipped
- Check layer — Cron Worker: HTTP checks, D1 time-series schema, KV snapshot.
- Display layer — Astro site, shadcn/ui component set, severity token system.
- Uptime bars & charts — 90-day adaptive timeline, per-component response-time graphs.
- Incidents — lifecycle model (Investigating → Identified → Monitoring → Resolved) + timeline UI.
- Notify layer (part 1) — Slack + generic webhook on status change, decoupled via Queues.
- Edge cache —
Cache-Tagemit + purge-on-change loop between the check and display layers. - Badges & public API — shields.io endpoint badges + JSON status API, edge-cached.
- Statuspage adapter — mirror any Atlassian Statuspage by page or component (guide).
- incident.io adapter — mirror any incident.io status page by page or component (guide).
- Statuspage webhooks — real-time ingest via
POST /webhooks/statuspage/:slug, cron as the backstop (guide).
In progress / planned
- TCP/SSL checks — extend the Cron Worker beyond HTTP (config + schema already accept them).
- Scheduled maintenance — distinct, forward-looking incident entries.
- Notify layer (part 2) — email + RSS/Atom feeds.
- Migration guide — importing an existing
.upptimerc.yml. - Vercel adapter path — documented alternative to Cloudflare.
- upptime/upptime — the serverless-monitoring idea this project builds on.
- Statuspage.io — the reference information architecture.
- Instatus — static-first delivery and modern design.
- statping/statping — a self-hosted, single-binary status server (Go) with its own monitoring engine, notifiers, and mobile app.
- OpenStatus — open-source synthetic monitoring and status pages, with a globally distributed checker for latency-aware probing.
- CachetHQ/Cachet — a long-standing open-source status page system (PHP/Laravel) centered on incident and component management.
Pull requests to StatusBeam are reviewed by two AI code reviewers, both free for open source:
- Greptile — free for non-commercial MIT/Apache projects under its OSS program.
- cubic — free for public repositories.
MIT