Source & license
Upstream license: MIT
License TL;DR
You can use it, change it, self-host it and sell it. Keep the original copyright and license notice with copies of the code. You don’t have to publish your changes. The authors don’t promise it will work.
Explain MIT in plain English →Summary of the main license. Separate packages and assets can have different terms.
Inspect repository ↗Read this project’s actual license ↗Repository owner
See the upstream repository for the original creator and contributors.
Maintain this project? Maintainer verification →Cloudflare hosting
Free tier eligible within limits
The documented StatusBeam deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs.
Hosting requirements
- Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits.
- Keep aggregate D1 use below 5 million rows read/day, 100,000 rows written/day and 5 GB total storage; a request can touch many rows.
- Keep KV below 100,000 reads/day, 1,000 writes, deletes and list operations/day each, and 1 GB; cache refreshes and backups consume writes.
- Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations.
Sources checked 01/10/2026
Repository snapshot: 4473885. Hosting eligibility reflects the deployment documentation and listed assumptions.
- better-stack ↗
🔗 **Live demo:** [demo.statusbeam.dev](https://demo.statusbeam.dev) — a StatusBeam instance monitoring a few public services, running on Cloudflare.
- uptimerobot ↗
🔗 **Live demo:** [demo.statusbeam.dev](https://demo.statusbeam.dev) — a StatusBeam instance monitoring a few public services, running on Cloudflare.
- pingdom ↗
🔗 **Live demo:** [demo.statusbeam.dev](https://demo.statusbeam.dev) — a StatusBeam instance monitoring a few public services, running on Cloudflare.
- workers ↗
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "statusbeam-web", // `main` and `assets` are injected by the @astrojs/cloudflare adapter at // build time; do not set them here (they would point at not-yet-built output). "compatibility_date": "2026-07-01", "compatibility_flags": ["nodejs_compat"], // Custom domain: Cloudflare provisions the proxied DNS record + edge cert // automatically (the `statusbeam.dev` z
- d1 ↗
{ "enabled": true }, // Same bindings as the check Worker — the page reads the snapshot/history. "d1_databases": [ { "binding": "DB", "database_name": "statusbeam", "database_id": "REPLACE_WITH_D1_DATABASE_ID" } ], "kv_namespaces": [ { "binding": "STATUS_KV", "id": "REPLACE_WITH_KV_NAMESPACE_ID" } ] }
- kv ↗
"DB", "database_name": "statusbeam", "database_id": "REPLACE_WITH_D1_DATABASE_ID" } ], "kv_namespaces": [ { "binding": "STATUS_KV", "id": "REPLACE_WITH_KV_NAMESPACE_ID" } ] }
- free-tier-eligible ↗
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "statusbeam-web", // `main` and `assets` are injected by the @astrojs/cloudflare adapter at // build time; do not set them here (they would point at not-yet-built output). "compatibility_date": "2026-07-01", "compatibility_flags": ["nodejs_compat"], // Custom domain: Cloudflare provisions the proxied DNS record + edge cert // automatically (the `statusbeam.dev` z
- free-tier-eligible ↗
{ "enabled": true }, // Same bindings as the check Worker — the page reads the snapshot/history. "d1_databases": [ { "binding": "DB", "database_name": "statusbeam", "database_id": "REPLACE_WITH_D1_DATABASE_ID" } ], "kv_namespaces": [ { "binding": "STATUS_KV", "id": "REPLACE_WITH_KV_NAMESPACE_ID" } ] }
- free-tier-eligible ↗
"DB", "database_name": "statusbeam", "database_id": "REPLACE_WITH_D1_DATABASE_ID" } ], "kv_namespaces": [ { "binding": "STATUS_KV", "id": "REPLACE_WITH_KV_NAMESPACE_ID" } ] }
- free-tier-eligible ↗
up>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 seconds)<br> Max of 15 minutes of CPU time per [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) or [Queue Consumer](https://developers.cloudflare.co
- free-tier-eligible ↗
oudflare.com/workers/platform/pricing/#workers) | | --- | --- | --- | | Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows | | Rows written | 100,000 / day | First 50 million / month included + $1.00 / million rows | | Storage (per GB stored) | 5 GB (total) | First 5 GB included + $0.75 / GB-mo | Track your D1 usage To accurately track your usage, use the [meta object](https://developers.cloudflare.com/d1/worker-api/return-object/), [GraphQL Analytics API](https://developers.cloudflare.com/d1/observability/metrics-analytics/#query-via-the-graphql-api), or the [Cloudflare dashboard ↗︎](https://dash.cloudflare.com/?to=/:account/workers/d1/). Select your D1 database, then vie
- free-tier-eligible ↗
cing/). | | Free plan<sup>1</sup> | Paid plan | | --- | --- | --- | | Keys read | 100,000 / day | 10 million/month, + $0.50/million | | Keys written | 1,000 / day | 1 million/month, + $5.00/million | | Keys deleted | 1,000 / day | 1 million/month, + $5.00/million | | List requests | 1,000 / day | 1 million/month, + $5.00/million | | Stored data | 1 GB | 1 GB, + $0.50/ GB-month | <sup>1</sup> The Workers Free plan includes limited Workers KV usage. All limits reset daily at 00:00 UTC. If you exceed any one of these limits, further operations of that type will fail with an error. Note Workers KV pricing for read, write and delete operations is on a per-key basis. Bulk read operations are billed by the amount
- MIT ↗
MIT License Copyright (c) 2026 pleaseai Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGE
- architecture ↗
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "statusbeam-web", // `main` and `assets` are injected by the @astrojs/cloudflare adapter at // build time; do not set them here (they would point at not-yet-built output). "compatibility_date": "2026-07-01", "compatibility_flags": ["nodejs_compat"], // Custom domain: Cloudflare provisions the proxied DNS record + edge cert // automatically (the `statusbeam.dev` z
- architecture ↗
{ "enabled": true }, // Same bindings as the check Worker — the page reads the snapshot/history. "d1_databases": [ { "binding": "DB", "database_name": "statusbeam", "database_id": "REPLACE_WITH_D1_DATABASE_ID" } ], "kv_namespaces": [ { "binding": "STATUS_KV", "id": "REPLACE_WITH_KV_NAMESPACE_ID" } ] }
- architecture ↗
"DB", "database_name": "statusbeam", "database_id": "REPLACE_WITH_D1_DATABASE_ID" } ], "kv_namespaces": [ { "binding": "STATUS_KV", "id": "REPLACE_WITH_KV_NAMESPACE_ID" } ] }
- architecture ↗
dules/wrangler/config-schema.json", "name": "statusbeam-worker", "main": "src/index.ts", "compatibility_date": "2026-07-01", "compatibility_flags": ["nodejs_compat"], // Run the check loop on a schedule. GitHub Actions cron is best-effort; // Cloudflare Cron Triggers fire on time. Default: every 5 minutes. "triggers": { "crons": ["*/5 * * * *"] }, // The Worker also serves a `fetch` handler for inbound provider webhooks at // `POST /webhooks/:provider/:slug` (src/webhook.ts) — real-time status updates // alongside the cron backstop. Supported providers: `statuspage` and `sentry`. // It's reachable at the *.workers.dev URL by default; authenticate it with a // shared secret: //
Documented public demo screenshot · pleaseai/statusbeam repository contributors; screenshot captured by Cloudsteading ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
HTTP availability checks, historical status and public status-page publication; synthetic-browser checks, real TCP/SSL probes and complete incident platforms are excluded.
See supporting source ↗HTTP availability checks, historical status and public status-page publication; synthetic-browser checks, real TCP/SSL probes and complete incident platforms are excluded.
See supporting source ↗HTTP availability checks, historical status and public status-page publication; synthetic-browser checks, real TCP/SSL probes and complete incident platforms are excluded.
See supporting source ↗How it works
The shape of StatusBeam on Cloudflare, and how it stacks up against the rented tools it replaces.
Architecture
Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.
View upstream source ↗Configuration and workflow sources
Reviewed commit 4473885671aa. Files were read as data; upstream applications and CI jobs were not executed.
Deployment configuration · 5 files
Cloudflare Pages · example/template, excluded from overview · compatibility 2026-07-01
statusbeam-docs · default
No resource bindings declared in this scope.
Cloudflare Workers · compatibility 2026-07-01
statusbeam-web · default
Configured route patterns: demo.statusbeam.dev
DB→ D1STATUS_KV→ KV
Cloudflare Workers · compatibility 2026-07-01
statusbeam-worker · default
Entrypoint: src/index.ts
Cron triggers (UTC): */5 * * * *
DB→ D1STATUS_KV→ KV
Cloudflare Workers · example/template, excluded from overview · compatibility 2026-07-01
statusbeam-web · default
Configured route patterns: status.example.com
DB→ D1STATUS_KV→ KV
Cloudflare Workers · example/template, excluded from overview · compatibility 2026-07-01
statusbeam-worker · default
Entrypoint: ./node_modules/@statusbeam/worker/src/index.ts
Cron triggers (UTC): */5 * * * *
DB→ D1STATUS_KV→ KV
Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.
Runtime source · handlers, binding usage and workflow steps
Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.
- L30 · scheduled handler exported · references SENTRY_AUTH_TOKEN · calls loadConfig, config.sites.filter, console.warn, Promise.all, sites.map, checkSite, ingest
- L55 · fetch handler exported · calls handleWebhook
- L59 · queue handler exported · calls consumeNotificationBatch
Environment references: env.SENTRY_AUTH_TOKEN
- L24 · buildNotificationMessages calls (conditional paths may differ): messages.push, toSlackMessage
- L57 · notify calls (conditional paths may differ): buildNotificationMessages, messages.slice, env.NOTIFY_QUEUE.sendBatch, chunk.map, console.error, postAll, console.warn, dispatchNotifications
- L109 · dispatchNotifications calls (conditional paths may differ): postAll, buildNotificationMessages
- L118 · postAll calls (conditional paths may differ): Promise.allSettled, messages.map, postJson
- L130 · consumeNotificationBatch calls (conditional paths may differ): Promise.all, batch.messages.map, deliverNotification, console.error, message.retry, message.ack
- L171 · deliverNotification calls (conditional paths may differ): fetchImpl, JSON.stringify, AbortSignal.timeout, redactUrl
- L200 · postJson calls (conditional paths may differ): deliverNotification, console.error, redactUrl
Environment references: env.NOTIFY_QUEUE
- L11 · loadConfig calls (conditional paths may differ): env.STATUS_KV.get, parseConfig
- L30 · ingest calls (conditional paths may differ): env.DB.batch, results.map, bind, env.DB.prepare, readSummary, results.filter, previous.get, writeSummary, changed.map, buildStatusChangePayload, toISOString, ctx.waitUntil, notify, purgeStatusCache, changes.map
- L74 · readSummary calls (conditional paths may differ): env.STATUS_KV.get, JSON.parse, summary.map
- L89 · writeSummary calls (conditional paths may differ): results.map, readHistory, config.sites.map, bySlug.get, previous.get, denseHistory, historyBySlug.get, formatUptime, windowUptime, history.slice, env.STATUS_KV.put, JSON.stringify
- L129 · readHistory calls (conditional paths may differ): since.setUTCDate, since.getUTCDate, since.setUTCHours, all, bind, env.DB.prepare, since.toISOString, bySlug.get, days.set, bySlug.set
- L158 · denseHistory calls (conditional paths may differ): d.setUTCDate, d.getUTCDate, slice, d.toISOString, out.push
Environment references: env.STATUS_KV · env.DB
- L19 · parseWebhookPath calls (conditional paths may differ): filter, pathname.split, includes
- L33 · timingSafeEqual calls (conditional paths may differ): a.charCodeAt, b.charCodeAt
- L52 · gradePayload calls (conditional paths may differ): sentryWebhookSchema.safeParse, deriveSentryWebhookStatus, statuspageWebhookSchema.safeParse, deriveStatuspageWebhookStatus
- L86 · handleWebhook calls (conditional paths may differ): parseWebhookPath, url.searchParams.get, timingSafeEqual, console.warn, loadConfig, config.sites.find, request.json, gradePayload, toISOString, ingest
Environment references: env.WEBHOOK_SECRET
- L19 · purgeStatusCache calls (conditional paths may differ): console.warn, cacheTags, fetchImpl, JSON.stringify, catch, res.text, console.error
Environment references: env.CF_API_TOKEN · env.CF_ZONE_ID
Build and deployment pipeline · 5 GitHub Actions workflows
Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.
Triggers: pull_request, push
Lint, typecheck, test, build · no job dependencies declared
- Checkout
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - Set up mise (node + bun)
jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 - Install dependencies
bun install --frozen-lockfile - Lint
bun run lint - Typecheck
bun run typecheck - Test with coverage
bun run test --coverage --reporter=junit --reporter-outfile=test-report.junit.xml - Build
bun run build - Upload coverage to Codecov
codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f - Upload test results to Codecov
codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72fCondition: ${{ !cancelled() }} - SonarQube Cloud scan
SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28fCondition: ${{ !cancelled() && env.SONAR_TOKEN != '' }}
Zizmor workflow lint · no job dependencies declared
- Checkout
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - Run zizmor
zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054
Triggers: workflow_dispatch, push
Deploy docs to Cloudflare · no job dependencies declared
- Preflight — required secrets present
missing="" [ -n "$CLOUDFLARE_API_TOKEN" ] || missing="$missing CLOUDFLARE_API_TOKEN(secret)" [ -n "$CLOUDFLARE_ACCOUNT_ID" ] || missing="$missing CLOUDFLARE_ACCOUNT_ID(secret)" if [ -n "$missing" ]; then echo "::error::Missing required config:$missing — see DEPLOYMENT.md" exit 1 fi - Checkout
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - Set up mise (node + bun)
jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 - Install dependencies
bun install --frozen-lockfile --ignore-scripts - Build docs (fail fast before touching production)
bun run --filter '@statusbeam/docs' build - Deploy docs
bun run --filter '@statusbeam/docs' deploy:publish
Triggers: workflow_dispatch
Deploy worker + web to Cloudflare · no job dependencies declared
- Preflight — required secrets/variables present
missing="" [ -n "$CLOUDFLARE_API_TOKEN" ] || missing="$missing CLOUDFLARE_API_TOKEN(secret)" [ -n "$CLOUDFLARE_ACCOUNT_ID" ] || missing="$missing CLOUDFLARE_ACCOUNT_ID(secret)" [ -n "$CF_D1_DATABASE_ID" ] || missing="$missing CF_D1_DATABASE_ID(variable)" [ -n "$CF_KV_NAMESPACE_ID" ] || missing="$missing CF_KV_NAMESPACE_ID(variable)" if [ -n "$missing" ]; then echo "::error::Missing required config:$missing — see DEPLOYMENT.md" exit 1 fi - Checkout
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - Set up mise (node + bun)
jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 - Install dependencies
bun install --frozen-lockfile --ignore-scripts - Inject account-specific resource IDs
for f in apps/worker/wrangler.jsonc apps/web/wrangler.jsonc; do sed -i "s|REPLACE_WITH_D1_DATABASE_ID|$CF_D1_DATABASE_ID|g; s|REPLACE_WITH_KV_NAMESPACE_ID|$CF_KV_NAMESPACE_ID|g" "$f" done - Build (fail fast before touching production)
bun run build - Apply D1 schema (idempotent — CREATE TABLE IF NOT EXISTS)
bun run --filter '@statusbeam/worker' db:apply:remote - Upload config to KV
if [ ! -f status.config.yml ]; then echo "::error::status.config.yml not found. Create it from status.config.example.yml (see DEPLOYMENT.md)." exit 1 fi bun run --filter '@statusbeam/worker' kv:config - Deploy check Worker
bun run --filter '@statusbeam/worker' deploy - Deploy status page
bun run --filter '@statusbeam/web' deploy:publish
Triggers: release, workflow_dispatch
Publish to npm (provenance) · no job dependencies declared
- Resolve package from the release tag
# tags: <component>-v<version> → strip the -v<version> suffix. component="${TAG%-v*}" case "$component" in core) dir="packages/core" ;; worker) dir="apps/worker" ;; web) dir="apps/web" ;; cli) dir="packages/cli" ;; create-statusbeam) dir="packages/create-statusbeam" ;; *) echo "::error::unknown release component '$component' (tag '$TAG')"; exit 1 ;; esac echo "dir=$dir" >> "$GITHUB_OUTPUT" echo "Publishing $dir for tag $TAG" - Checkout
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - Set up bun
oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 - Set up node
actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - Install dependencies
bun install --frozen-lockfile --ignore-scripts - Quality gate
bun run lint bun run typecheck bun run test - Build
bun run build - Pack + publish with provenance
dest="$RUNNER_TEMP/pack" mkdir -p "$dest" # bun pm pack rewrites workspace:* deps to real versions in the tarball. bun pm pack --destination "$dest" tarball="$(find "$dest" -name '*.tgz' | head -1)" if [ -z "$tarball" ]; then echo "::error::no tarball produced by bun pm pack"; exit 1 fi # Publish from outside the repo checkout: inside the Bun workspaces # monorepo npm enters workspace mode and `npm publish <tarball>` dies # with ENOWORKSPACES. No token/config-set — auth is the trusted # publish…
Triggers: push, workflow_dispatch
Release PR + GitHub Release · no job dependencies declared
- Mint release-bot token
actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 - Release please
googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7
build: turbo run builddeploy: bun run --filter '@statusbeam/worker' deploy && bun run --filter '@statusbeam/web' deploy
build: astro builddeploy: astro build && wrangler pages deploy --branch=maindeploy:publish: wrangler pages deploy --branch=main
build: astro builddeploy: astro build && wrangler deploydeploy:publish: wrangler deploy
deploy: wrangler deploybuild: tsc --noEmit
build: bun build ./src/cli.ts --outdir dist --target node --format esm --banner '#!/usr/bin/env node' && chmod +x dist/cli.js
build: bun build ./src/index.ts --outdir dist --target node --format esm --external yaml --external zod && tsc -p tsconfig.build.json
build: bun build ./src/index.ts --outdir dist --target node --format esm --banner '#!/usr/bin/env node' && chmod +x dist/index.js
Repository README
View original on GitHub ↗Full upstream document by @pleaseai · README.md · snapshot 4473885
StatusBeam
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.
Status
🚧 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.
How it works
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.
Tech stack
| 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.
Design
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.
Project layout
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)
Configuration
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 | sentry | aigateway
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)
Check types
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. |
sentry |
Mirrors a Sentry Uptime monitor via issue webhook (real-time) + optional poll backstop. See the Sentry adapter guide. |
aigateway |
Reads a model endpoint's published health from the Vercel AI Gateway or OpenRouter. See the AI Gateway 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 Sentry adapter presents a Sentry Uptime
monitor on your status page. Sentry runs the checks; StatusBeam ingests the
verdict in real time from a Sentry issue webhook (POST /webhooks/sentry/:slug),
with an optional cron poll of Sentry's Issues API as the backstop. Binary
(up/down). Setup, status mapping, and the webhook-only vs. poll modes:
docs/adapters/sentry.md.
The AI Gateway adapter puts a model's health on your status page by reading
what the Vercel AI Gateway or
OpenRouter already publishes about the providers
serving it — uptime and latency from their own production traffic. It sends no
probe requests to the model, so it spends no tokens and needs no API key.
Grade the whole model (best endpoint wins, since the gateway routes around a
failing provider) or one provider endpoint by name. Note the two gateways
aggregate over different windows, and OpenRouter currently publishes no latency:
docs/adapters/aigateway.md.
Internationalization
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.
Badges & public API
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).
Badges
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.
Status API
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.
Deployment
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.
Instant cache invalidation (optional)
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 page
When 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.
Roadmap
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). - Sentry Uptime adapter — mirror a Sentry Uptime monitor via issue webhook (
POST /webhooks/sentry/:slug) + optional Issues-API poll backstop (guide). - AI Gateway adapter — track a model endpoint's published health on the Vercel AI Gateway or OpenRouter, token-free (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.
Prior art & inspiration
- 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.
Code review
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.
License
MIT
Frequently asked about StatusBeam
What is StatusBeam?+
StatusBeam is a self-hosted Better Stack/Pingdom alternative built on the Cloudflare developer platform. Run HTTP uptime checks and publish a status page using two Cloudflare Workers.
What does StatusBeam replace?+
StatusBeam is listed as an alternative to Better Stack, Pingdom, UptimeRobot. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does StatusBeam use?+
StatusBeam is built on D1, KV, Workers.
How much does StatusBeam cost to run?+
The documented StatusBeam deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs. Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits. Keep aggregate D1 use below 5 million rows read/day, 100,000 rows written/day and 5 GB total storage; a request can touch many rows. Keep KV below 100,000 reads/day, 1,000 writes, deletes and list operations/day each, and 1 GB; cache refreshes and backups consume writes. Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations. Check current Cloudflare pricing before deploying.
Is StatusBeam open source?+
The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/pleaseai/statusbeam/4473885671aa0227c21ac4e08d69d5023c7c0879/LICENSE. Source code and contributor credit are available at https://github.com/pleaseai/statusbeam.




Discussion · 0
sign in to comment →