Cloudsteading
StatusBeam’s documented demo showing service uptime histories and status indicators; a snapshot of the public demo, not a new self-hosting verification.

StatusBeam

Run HTTP uptime checks and publish a status page using two Cloudflare Workers.

StatusBeam is a self-hosted Better Stack/Pingdom alternative built on Cloudflare (D1, KV, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.

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

@pleaseai

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.
Check current pricing ↗
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.

Better Stack logoBetter Stack ↗

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 ↗
UptimeRobot logoUptimeRobot ↗

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 ↗
Pingdom logoPingdom ↗

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 ↗
external SaaS target
varies
→ D1 + KV + Workers
external SaaS target
varies
→ D1 + KV + Workers
external SaaS target
varies
→ D1 + KV + Workers

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 ↗
Public interface
Configured entry points2
statusbeam-web
apps/web/wrangler.jsonc
statusbeam-worker
apps/worker/wrangler.jsonc
↓
App
statusbeam-web
entry
Cloudflare Workers
statusbeam-worker
entry
Cloudflare Workers
Entrypoint: src/index.tsConfigured cron (UTC): */5 * * * *
↓

Configuration and workflow sources

Reviewed commit 4473885671aa. Files were read as data; upstream applications and CI jobs were not executed.

Deployment configuration · 5 files
apps/docs/wrangler.jsonc ↗

Cloudflare Pages · example/template, excluded from overview · compatibility 2026-07-01

statusbeam-docs · default

    No resource bindings declared in this scope.

    apps/web/wrangler.jsonc ↗

    Cloudflare Workers · compatibility 2026-07-01

    statusbeam-web · default

    Configured route patterns: demo.statusbeam.dev

    • DB → D1
    • STATUS_KV → KV
    apps/worker/wrangler.jsonc ↗

    Cloudflare Workers · compatibility 2026-07-01

    statusbeam-worker · default

    Entrypoint: src/index.ts

    Cron triggers (UTC): */5 * * * *

    • DB → D1
    • STATUS_KV → KV
    packages/create-statusbeam/templates/wrangler.web.jsonc ↗

    Cloudflare Workers · example/template, excluded from overview · compatibility 2026-07-01

    statusbeam-web · default

    Configured route patterns: status.example.com

    • DB → D1
    • STATUS_KV → KV
    packages/create-statusbeam/templates/wrangler.worker.jsonc ↗

    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 → D1
    • STATUS_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.

    apps/worker/src/index.ts ↗
    • 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

    apps/worker/src/notify.ts ↗
    • 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

    apps/worker/src/ingest.ts ↗
    • 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

    apps/worker/src/webhook.ts ↗
    • 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

    apps/worker/src/cache.ts ↗
    • 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.

    CI · .github/workflows/ci.yml ↗

    Triggers: pull_request, push

    Lint, typecheck, test, build · no job dependencies declared

    1. Checkoutactions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
    2. Set up mise (node + bun)jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654
    3. Install dependenciesbun install --frozen-lockfile
    4. Lintbun run lint
    5. Typecheckbun run typecheck
    6. Test with coveragebun run test --coverage --reporter=junit --reporter-outfile=test-report.junit.xml
    7. Buildbun run build
    8. Upload coverage to Codecovcodecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f
    9. Upload test results to Codecovcodecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72fCondition: ${{ !cancelled() }}
    10. SonarQube Cloud scanSonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28fCondition: ${{ !cancelled() && env.SONAR_TOKEN != '' }}

    Zizmor workflow lint · no job dependencies declared

    1. Checkoutactions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
    2. Run zizmorzizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054
    Deploy Docs · .github/workflows/deploy-docs.yml ↗

    Triggers: workflow_dispatch, push

    Deploy docs to Cloudflare · no job dependencies declared

    1. Preflight — required secrets presentmissing="" [ -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
    2. Checkoutactions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
    3. Set up mise (node + bun)jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654
    4. Install dependenciesbun install --frozen-lockfile --ignore-scripts
    5. Build docs (fail fast before touching production)bun run --filter '@statusbeam/docs' build
    6. Deploy docsbun run --filter '@statusbeam/docs' deploy:publish
    Deploy · .github/workflows/deploy.yml ↗

    Triggers: workflow_dispatch

    Deploy worker + web to Cloudflare · no job dependencies declared

    1. Preflight — required secrets/variables presentmissing="" [ -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
    2. Checkoutactions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
    3. Set up mise (node + bun)jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654
    4. Install dependenciesbun install --frozen-lockfile --ignore-scripts
    5. Inject account-specific resource IDsfor 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
    6. Build (fail fast before touching production)bun run build
    7. Apply D1 schema (idempotent — CREATE TABLE IF NOT EXISTS)bun run --filter '@statusbeam/worker' db:apply:remote
    8. Upload config to KVif [ ! -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
    9. Deploy check Workerbun run --filter '@statusbeam/worker' deploy
    10. Deploy status pagebun run --filter '@statusbeam/web' deploy:publish
    Publish · .github/workflows/publish.yml ↗

    Triggers: release, workflow_dispatch

    Publish to npm (provenance) · no job dependencies declared

    1. 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"
    2. Checkoutactions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
    3. Set up bunoven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
    4. Set up nodeactions/setup-node@820762786026740c76f36085b0efc47a31fe5020
    5. Install dependenciesbun install --frozen-lockfile --ignore-scripts
    6. Quality gatebun run lint bun run typecheck bun run test
    7. Buildbun run build
    8. Pack + publish with provenancedest="$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…
    Release Please · .github/workflows/release-please.yml ↗

    Triggers: push, workflow_dispatch

    Release PR + GitHub Release · no job dependencies declared

    1. Mint release-bot tokenactions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
    2. Release pleasegoogleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7
    package.json ↗
    • build: turbo run build
    • deploy: bun run --filter '@statusbeam/worker' deploy && bun run --filter '@statusbeam/web' deploy
    apps/docs/package.json ↗
    • build: astro build
    • deploy: astro build && wrangler pages deploy --branch=main
    • deploy:publish: wrangler pages deploy --branch=main
    apps/web/package.json ↗
    • build: astro build
    • deploy: astro build && wrangler deploy
    • deploy:publish: wrangler deploy
    apps/worker/package.json ↗
    • deploy: wrangler deploy
    • build: tsc --noEmit
    packages/cli/package.json ↗
    • build: bun build ./src/cli.ts --outdir dist --target node --format esm --banner '#!/usr/bin/env node' && chmod +x dist/cli.js
    packages/core/package.json ↗
    • build: bun build ./src/index.ts --outdir dist --target node --format esm --external yaml --external zod && tsc -p tsconfig.build.json
    packages/create-statusbeam/package.json ↗
    • build: bun build ./src/index.ts --outdir dist --target node --format esm --banner '#!/usr/bin/env node' && chmod +x dist/index.js

    Full upstream document by @pleaseai · README.md · snapshot 4473885

    StatusBeam

    CI Quality Gate Status codecov License: MIT Reviewed by Greptile Reviewed by cubic Live demo Deploy on Cloudflare

    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:

    1. their remembered choice (a locale cookie, set when they pick a language),
    2. their browser's Accept-Language,
    3. the deployment's theme.locale (used only when the above don't match a supported language),
    4. 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
    ![status](https://img.shields.io/endpoint?url=https://status.example.com/api/badge.json)
    ![uptime](https://img.shields.io/endpoint?url=https://status.example.com/api/badge/api/uptime.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-up status plus 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-Tag emit + 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.

    Reviewed by Greptile Reviewed by cubic


    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 →
    No comments yet — be the first.