Cloudsteading
Product image still needed. This listing has source documentation, but no reviewed screenshot yet.

Uptime Scarecrow

HTTP and heartbeat monitoring, incident alerts and public status pages on Cloudflare.

Uptime Scarecrow is a self-hosted Statuspage/UptimeRobot alternative built on Cloudflare (Access, D1, Durable Objects, Email Workers, KV). Paid services required. 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

@cloudsteading

See the upstream repository for the original creator and contributors.

Maintain this project? Maintainer verification →

Cloudflare hosting

Paid services required

The documented installation requires Workers Paid, from $5 USD per account/month plus usage overages. No zero-cost or fixed total monthly bill is claimed.

Hosting requirements
  • Create the notification and dead-letter queues before deployment; apply the D1 schema afterwards.
  • Set up Cloudflare Access before using admin routes; leave the development authentication bypass disabled in production.
  • Disable optional Workers AI summaries if not needed; model usage and notification destinations may have separate limits or costs.
  • Email alerts require a Cloudflare zone with a verified sending domain; domain registration is a separate expense.
  • Monitor count and check frequency affect Durable Object, Worker, queue and D1 usage. No fresh deployment or metered workload was tested.
Check current pricing ↗
Sources checked 01/10/2026

Repository snapshot: 6cfc3b9. Hosting eligibility reflects the deployment documentation and listed assumptions.

  • uptimerobot ↗

    **HTTP monitors** — schedule

  • statuspage ↗

    **Public status page** at `/` — standard statuspage UX.

  • workers ↗

    Two Workers in one repo:

  • d1 ↗

    **D1 (`uptime-scarecrow-db`)**

  • kv ↗

    **KV (`CACHE`)**

  • durable-objects ↗

    hosts Durable Object classes (`MonitorScheduler`, `HeartbeatTracker`)

  • queues ↗

    **Queue (`uptime-scarecrow-notifications`)**

  • access ↗

    Cloudflare Access JWT

  • workers-ai ↗

    **Workers AI** — incident summaries.

  • email-workers ↗

    **Email Routing → `SEND_EMAIL` binding** — outbound alerts.

  • paid ↗

    Cloudflare account on the **Workers Paid** plan ($5/mo

  • paid ↗

    for a minimum charge of $5 USD per month for an account.

  • MIT ↗

    MIT License Copyright (c) 2026 Cloudsteading

  • architecture ↗

    The app talks to DOs that live *inside* the scheduler Worker (cross-Worker DO bindings via `script_name`). Both Workers share the same D1, KV, and Queue.

  • architecture ↗

    Per-minute cron, daily rollup/prune crons, queue consumer for notifications, hosts Durable Object classes

What it can replace

Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.

external SaaS target
varies
external SaaS target
varies

How it works

The shape of Uptime Scarecrow on Cloudflare, and how it stacks up against the rented tools it replaces.

Architecture

Diagram based on the linked repository documentation. See the sources and hosting assumptions above.

View upstream source ↗
Public interface
Monitor and operate2
Public status page and heartbeat endpoint
HTTP checks and job heartbeat pings
Admin dashboard and API
Protected by Cloudflare Access
↓
App
uptime-scarecrow-app
entry
Astro on Workers
Dashboard, status pages and heartbeat ingest
delegates to↓
uptime-scarecrow-scheduler
backing
Cloudflare Workers
Durable Object check alarmsNotification queue consumerDaily rollup and prune
↓

Configuration and workflow sources

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

Deployment configuration · 2 files
wrangler.jsonc ↗

Cloudflare Workers · compatibility 2026-04-01

uptime-scarecrow-app · default

  • DB → D1
  • CACHE → KV
  • MONITOR_SCHEDULER → Durable Objects · class MonitorScheduler · Worker uptime-scarecrow-scheduler
  • HEARTBEAT_TRACKER → Durable Objects · class HeartbeatTracker · Worker uptime-scarecrow-scheduler
  • AI → Workers AI
  • NOTIFY_QUEUE → Queues (producer) · queue uptime-scarecrow-notifications
  • SEND_EMAIL → Send Email
wrangler.scheduler.jsonc ↗

Cloudflare Workers · compatibility 2026-04-01

uptime-scarecrow-scheduler · default

Entrypoint: src/scheduler/index.ts

Cron triggers (UTC): * * * * * · 5 0 * * * · 15 0 * * *

  • DB → D1
  • CACHE → KV
  • MONITOR_SCHEDULER → Durable Objects · class MonitorScheduler
  • HEARTBEAT_TRACKER → Durable Objects · class HeartbeatTracker
  • uptime-scarecrow-notifications → Queues (consumer) · queue uptime-scarecrow-notifications · dead letters uptime-scarecrow-notifications-dlq
  • SEND_EMAIL → Send Email

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.

src/scheduler/index.ts ↗
  • L19 · fetch handler exported · references APP_VERSION, DB, CACHE, MONITOR_SCHEDULER, HEARTBEAT_TRACKER, SEND_EMAIL · calls Response.json
  • L45 · scheduled handler exported · calls ctx.waitUntil, rollupYesterday, pruneRecentChecks, armAlarms
  • L58 · queue handler exported · calls dispatchQueueMessage, msg.ack, console.error, msg.retry
  • L71 · armAlarms calls (conditional paths may differ): all, env.DB.prepare, ns.get, ns.idFromName, stub.fetch, JSON.stringify, console.warn
  • L98 · rollupYesterday calls (conditional paths may differ): Math.floor, Date.now, all, bind, env.DB.prepare, incByMon.set, lats.results.map, Math.min, Math.max, Math.ceil, incByMon.get, run, pick, console.log
  • L222 · pruneRecentChecks calls (conditional paths may differ): Math.floor, Date.now, run, bind, env.DB.prepare, console.log
  • L230 · dispatchQueueMessage calls (conditional paths may differ): first, bind, env.DB.prepare, JSON.parse, dispatchNotification

Environment references: env.APP_VERSION · env.DB · env.CACHE · env.MONITOR_SCHEDULER · env.HEARTBEAT_TRACKER · env.SEND_EMAIL · env.APP_BASE_URL

Build and deployment pipeline · 0 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.

No GitHub Actions workflow was found in the collected tree. Deployment may be manual or configured elsewhere.

package.json ↗
  • build: astro build
  • deploy:app: astro build && wrangler deploy
  • deploy:scheduler: wrangler deploy -c wrangler.scheduler.jsonc
  • deploy: npm run deploy:scheduler && npm run deploy:app

Full upstream document by @cloudsteading · README.md · snapshot 6cfc3b9

Uptime Scarecrow

Self-hosted uptime monitoring built entirely on Cloudflare — by Cloudsteading.

Stands watch over your URLs and cron jobs and squawks when something's wrong. Two Workers, one D1 database, no third parties. Deploys to your own Cloudflare account in a few minutes; expect to pay $2–5/month at indie scale.

Deploy to Cloudflare

The Deploy button forks this repo into your GitHub, connects it to your Cloudflare account, and runs npm run deploy. D1, KV, and DO resources auto-provision on first run. Queues need to be created once before the first deploy — see § Quick install for the full step list.


Features

  • HTTP monitors — schedule (1m / 5m / 15m / hourly / any cron); assert on status code, body keyword, response time, and TLS expiry. Per-token rate-limited heartbeat ingest at /h/<token>.
  • Heartbeat monitors — your job pings Uptime Scarecrow; alerts when the ping goes silent past a configurable grace window.
  • Incidents — auto-opened on DOWN, auto-closed on recovery, optionally summarised by Workers AI.
  • Notifications — Cloudflare Email Service, Slack, Discord, signed JSON webhooks. PagerDuty + Telegram are stubbed for v0.2.
  • Public status page at / — standard statuspage UX. Per-monitor is_public flag; private by default.
  • Admin under /admin/* — Cloudflare Access in front, no app-side login UI.

Quick install

You'll need a Cloudflare account on the Workers Paid plan ($5/mo — required for Durable Objects + Queues), Node ≥ 22.12, and wrangler authenticated (npx wrangler login).

# 1. Clone and install
git clone https://github.com/cloudsteading/uptime-scarecrow.git
cd uptime-scarecrow
nvm use && npm install

# 2. Create the two queues (these are the only resources wrangler can't auto-provision yet)
npx wrangler queues create uptime-scarecrow-notifications
npx wrangler queues create uptime-scarecrow-notifications-dlq

# 3. First deploy. wrangler auto-creates D1, KV, and the DO classes on demand.
npm run deploy

# 4. Apply schema to the freshly-created remote D1
npm run db:migrate:remote

The app is now reachable at https://uptime-scarecrow-app.<your-subdomain>.workers.dev. Public status page works immediately; /admin returns 503 until you set up Cloudflare Access in step 5.

# 5. Set required secrets — see "Production setup" below for how to obtain each value
npx wrangler secret put ACCESS_TEAM_DOMAIN     # e.g. your-team.cloudflareaccess.com
npx wrangler secret put ACCESS_AUD             # 64-char hex from your Access app
npx wrangler secret put EMAIL_FROM             # e.g. alerts@your-domain.com

After that, https://uptime-scarecrow-app.<your-subdomain>.workers.dev/admin redirects through your IdP and into the dashboard.

For a custom domain, see § Custom domain below.


Architecture

Two Workers in one repo:

Worker Config Role
uptime-scarecrow-app wrangler.jsonc Astro server output — public status page (/, /m/<id>), Access-gated admin dashboard (/admin/*), JSON API (/admin/api/v1/*), heartbeat ingest (/h/<token>).
uptime-scarecrow-scheduler wrangler.scheduler.jsonc Per-minute cron, daily rollup/prune crons, queue consumer for notifications, hosts Durable Object classes (MonitorScheduler, HeartbeatTracker).

The app talks to DOs that live inside the scheduler Worker (cross-Worker DO bindings via script_name). Both Workers share the same D1, KV, and Queue.

Routes at a glance

Path Auth Purpose
/ public Status page — public monitors, banner, recent incidents
/m/<id> public Public monitor detail (404 if is_public = 0)
/h/<token> public, rate-limited Heartbeat ingest
/admin Access Admin dashboard (all monitors)
/admin/incidents, /admin/settings, /admin/monitor/*, /admin/incident/* Access Operator views
/admin/api/v1/* Access JSON / form API
HTTP / heartbeat
       │
       ▼
[ uptime-scarecrow-app ]  ◄──── Cloudflare Access (JWT)
       │
       ├── DB writes ──► D1
       ├── ensure DO  ──► [ MonitorScheduler DO ]  (in scheduler worker)
       │                       │  alarm fires every interval_seconds
       │                       ▼
       │                  performCheck()
       │                       │
       │                       ├── recent_check (8d ring)  ──► D1
       │                       └── state machine ──► incident open/close
       │                                                │
       │                                                ▼
       │                                         NOTIFY_QUEUE
       │                                                │
       └────────────────────────── [ scheduler queue consumer ]
                                                        │
                                                        ▼
                                          email / slack / discord / webhook

Storage model

  • D1 (uptime-scarecrow-db) — monitors, incidents, channels, users, audit log, recent_check (8-day rolling per-check ring) and check_daily (forever rollups).
  • KV (CACHE) — JWKS cache for Access JWT verification, status-page cache.
  • Queue (uptime-scarecrow-notifications) — decouples check state changes from notification fan-out, with DLQ.
  • Rate-limit binding — caps /h/<token> heartbeat ingest at 30 req/min per token.
  • Workers AI — incident summaries.
  • Email Routing → SEND_EMAIL binding — outbound alerts.

There is no R2, no Analytics Engine, no Postgres, no Redis. Timeseries are D1-only.


Local development

git clone https://github.com/cloudsteading/uptime-scarecrow.git
cd uptime-scarecrow
nvm use && npm install

# Local auth bypass — gitignored, never committed.
cp .dev.vars.example .dev.vars      # contains DEV_NO_AUTH=1

# Local D1 schema
npm run db:migrate:local

# App + scheduler concurrently. Auto-arms DO alarms on startup.
npm run dev

See docs/dev.md for the full local workflow.


Production setup

The Quick install above gets the workers deployed and resources auto-provisioned. The remaining work is connecting Cloudflare Access (so /admin works) and Email Routing (so alerts deliver). Both are dashboard tasks.

Cloudflare Access (gates /admin)

Access only intercepts /admin/* — public status page stays open. In the Zero Trust dashboard:

  1. Settings → Authentication → Login methods → Add new — pick at least one IdP. Google or GitHub OAuth are most reliable; one-time PIN works but depends on email deliverability.
  2. Access → Applications → Add an application → Self-hosted.
  3. Application domain — your worker's hostname (the workers.dev URL or your custom domain) with the Path set to admin. This is critical: it scopes Access to admin only.
  4. Identity providers — check the IdP from step 1.
  5. Add a policy — Allow action, Include → Emails → your operator emails.
  6. Save. On the app's Overview tab, copy:
    • AUD tag → npx wrangler secret put ACCESS_AUD
    • Team domain (e.g. your-team.cloudflareaccess.com) → npx wrangler secret put ACCESS_TEAM_DOMAIN

The middleware fails closed if either secret is missing — /admin returns 503 rather than silently granting access.

Email Routing (powers notifications)

The send_email binding requires the from-domain to be on a Cloudflare zone with Email Routing enabled. To avoid colliding with your existing mail (e.g. Google Workspace on the apex), use a dedicated subdomain like m.your-domain.com.

  1. Cloudflare dashboard → your zone (or subdomain zone) → Email → Email Routing → Get Started.
  2. CF auto-adds MX + SPF + DKIM records. Click Add records and enable.
  3. Add a destination address (any real inbox you control) and verify it via the email link.
  4. Pick a from-address (e.g. alerts@m.your-domain.com) — it doesn't need to be a real inbox; the binding just needs the zone enabled.
npx wrangler secret put EMAIL_FROM                              # e.g. alerts@m.your-domain.com
npx wrangler secret put EMAIL_FROM -c wrangler.scheduler.jsonc  # the queue consumer also sends mail

Test from the dashboard: /admin/settings → Send test email to me.

Optional secrets

npx wrangler secret put APP_BASE_URL              # e.g. https://uptime.your-domain.com — used in alert email links
npx wrangler secret put BOOTSTRAP_ADMIN_EMAILS    # comma-separated — these get is_admin=1 on first login
npx wrangler secret put ALLOWED_EMAILS            # comma-separated allowlist; defense-in-depth on top of Access

DEV_NO_AUTH must NEVER be set in production. The middleware fails closed when ACCESS_TEAM_DOMAIN/ACCESS_AUD are missing unless DEV_NO_AUTH=1, so a forgotten Access config returns 503 rather than silently granting admin.

Custom domain

Add a domain like uptime.your-domain.com either way:

Dashboard — Workers & Pages → uptime-scarecrow-app → Settings → Domains & Routes → Add → Custom Domain. Hostname must be a CF zone in the same account; DNS is auto-created. Persists across deploys without committing anything.

Wrangler config — uncomment + edit the routes block in wrangler.jsonc:

"routes": [
  { "pattern": "uptime.example.com", "custom_domain": true }
]

…then redeploy. Note: with routes in config, wrangler manages the binding declaratively — removing it later detaches the domain on next deploy. Dashboard-added domains are independent.

Smoke-check

  • Visit / — you should see "No public monitors" on the public status page.
  • Visit /admin — should redirect through your IdP and into an empty Monitors dashboard.
  • Add an HTTP monitor pointed at a known-good URL. Wait ~5s — the bar starts filling.
  • Settings → Send test email to me — confirms SEND_EMAIL wiring end-to-end.

Notification channels

Configured under Settings in the dashboard. Each channel row stores its config as JSON in D1.

Channel recipient shape config
email email address {}
slack incoming webhook URL {}
discord webhook URL {}
webhook webhook URL { "url": "...", "secret": "..." } — if secret is set, the request includes X-Scarecrow-Signature: t=<unix>,v1=<hex_hmac_sha256("<unix>.<body>")> (Stripe-style; receiver must verify timestamp freshness)

Webhook payload:

{
  "kind": "incident.open" | "incident.recover" | "test",
  "monitor": { "id": 7, "name": "API", "url": "https://api.example.com/health" },
  "incident_id": 42,
  "cause": "HTTP 502 (Bad Gateway)",
  "timestamp": 1730000000,
  "dashboard_url": "https://uptime.example.com/monitor/7"
}

Configuration reference

Worker secrets

Name Required? Purpose
ACCESS_TEAM_DOMAIN yes (prod) e.g. your-team.cloudflareaccess.com. Used to fetch JWKS for JWT verification.
ACCESS_AUD yes (prod) AUD tag for the Access application protecting this Worker.
EMAIL_FROM yes (for email alerts) The verified Email Routing address used as the From: header.
ALLOWED_EMAILS no Comma-separated email allowlist enforced after Access (defence in depth).
BOOTSTRAP_ADMIN_EMAILS no Emails matching this list get is_admin=1 on first login.
APP_BASE_URL no Used to build dashboard links inside alert emails / webhook payloads.
DEV_NO_AUTH dev only Set to 1 in .dev.vars to bypass Access locally. NEVER set in prod.

Vars (in wrangler config)

Name Default Purpose
APP_NAME uptime-scarecrow Used in user-agent strings and audit logs.
APP_VERSION 0.1.0 Same.

Cron schedules

The scheduler worker fires three crons (defined in wrangler.scheduler.jsonc):

Cron What it does
* * * * * Per-minute safety net — re-arms DO alarms for any active monitor that doesn't have one.
5 0 * * * Daily rollup — yesterday's recent_check rows → check_daily.
15 0 * * * Daily prune — drops recent_check rows older than 8 days.

Per-monitor checks are driven by Durable Object alarms, not by these crons. Once a monitor's DO calls setAlarm(now + interval_seconds * 1000), workerd fires it on schedule and the DO re-arms itself. The minute-cron is a safety net for cases where alarms get cleared (deploys, etc.).


Security

A summary of the security boundaries — see src/middleware.ts, src/lib/access.ts, and src/lib/checks.ts for the implementation.

  • Auth. Cloudflare Access JWT (verified via JWKS, RS256) is required only for /admin and /admin/* (which covers /admin/api/v1/*). Email allowlist enforced after JWT verification. The public status page (/, /m/<id>) and /h/<token> are public by design — anyone can hit them, but the public monitor detail returns 404 unless the monitor's is_public column is 1.
  • SSRF. Outbound check fetches block localhost, 127.0.0.0/8, ::1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 (incl. cloud metadata 169.254.169.254), 100.64.0.0/10, fc00::/7, and DoH-resolve before fetch with redirect re-validation.
  • Heartbeat tokens. 32 bytes from crypto.getRandomValues, stored as SHA-256 hash. Lookup is constant-time hash-compare. Per-token rate limit at 30 req/min.
  • Webhook signatures. Optional HMAC-SHA256 over <unix-ts>.<body>, sent as X-Scarecrow-Signature: t=<unix>,v1=<hex>. Receivers must verify timestamp freshness to prevent replay.
  • CSRF. Same-origin policy + Access JWT on all mutating routes. No anti-CSRF token because Access cookies are scoped to the Access application's hostname.
  • Audit log. Every mutation (channel CRUD, monitor CRUD, email send) writes a row to audit_log with actor email and IP.

For the local-dev auth bypass: see the DEV_NO_AUTH note above. The middleware fails closed if Access env vars are missing in production, so a misconfigured deploy returns 503 rather than granting access.


Project layout

.
├── db/migrations/        # 0001_init.sql + later migrations
├── docs/
│   ├── dev.md            # local development guide
│   └── specs/timeseries.md
├── scripts/
│   └── smoke-test.sh
├── src/
│   ├── components/       # Astro components (charts, logo, cron preview)
│   ├── do/               # Durable Object classes
│   ├── layouts/
│   ├── lib/
│   │   ├── access.ts     # Cloudflare Access JWT verification
│   │   ├── checks.ts     # HTTP check execution + SSRF guards
│   │   ├── monitors.ts   # monitor CRUD + heartbeat token lifecycle
│   │   ├── notify/       # channel adapters (email/slack/discord/webhook)
│   │   └── …
│   ├── middleware.ts     # Astro middleware: Access JWT → ctx.locals.user
│   ├── pages/
│   │   ├── api/v1/       # JSON API
│   │   ├── app/          # dashboard
│   │   ├── h/[token].ts  # heartbeat ingest (public, rate-limited)
│   │   └── …
│   ├── scheduler/        # the scheduler Worker entrypoint
│   ├── styles/global.css # Tailwind v4 + theme tokens
│   └── env.d.ts
├── wrangler.jsonc          # app worker
└── wrangler.scheduler.jsonc # scheduler worker

License

MIT.


Contributing

This is an early-stage, single-tenant project — Cloudsteading uses it for our own monitoring, and we're sharing it for anyone else to deploy on their own Cloudflare account. Issues and PRs welcome at github.com/cloudsteading/uptime-scarecrow.

A few notes for contributors:

  • Branch from main, ship small focused PRs. Substantial changes should land an issue first to confirm scope.
  • Local dev: see docs/dev.md. One command (npm run dev) runs both workers, auto-arms DO alarms, and gives you a /admin you can browse via the DEV_NO_AUTH=1 bypass.
  • Schema changes go in db/migrations/NNNN_<description>.sql (sequential numbering). Run npm run db:migrate:local to apply.
  • Type-check before pushing: npx tsc --noEmit. We don't have a full test suite yet — scripts/smoke-test.sh is the closest thing.
  • docs/specs/ is where v0.2 design notes go before they become migrations / code.

Roughly what's on the v0.2 list (file an issue if you want to tackle one): PagerDuty + Telegram channel adapters, comments on incidents, configurable header navigation, daily check_daily fallback for chart data older than 8 days, status-page subscriber emails, monitor groups.

Frequently asked about Uptime Scarecrow

What is Uptime Scarecrow?+

Uptime Scarecrow is a self-hosted Statuspage/UptimeRobot alternative built on the Cloudflare developer platform. HTTP and heartbeat monitoring, incident alerts and public status pages on Cloudflare.

What does Uptime Scarecrow replace?+

Uptime Scarecrow is listed as an alternative to Statuspage, UptimeRobot. Compare the features and tradeoffs before migrating.

What Cloudflare primitives does Uptime Scarecrow use?+

Uptime Scarecrow is built on Access, D1, Durable Objects, Email Workers, KV, Queues, Workers, Workers AI.

How much does Uptime Scarecrow cost to run?+

The documented installation requires Workers Paid, from $5 USD per account/month plus usage overages. No zero-cost or fixed total monthly bill is claimed. Create the notification and dead-letter queues before deployment; apply the D1 schema afterwards. Set up Cloudflare Access before using admin routes; leave the development authentication bypass disabled in production. Disable optional Workers AI summaries if not needed; model usage and notification destinations may have separate limits or costs. Email alerts require a Cloudflare zone with a verified sending domain; domain registration is a separate expense. Monitor count and check frequency affect Durable Object, Worker, queue and D1 usage. No fresh deployment or metered workload was tested. Check current Cloudflare pricing before deploying.

Is Uptime Scarecrow open source?+

The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/cloudsteading/uptime-scarecrow/6cfc3b94e0f687440f02eca11606d3be7ea43440/LICENSE. Source code and contributor credit are available at https://github.com/cloudsteading/uptime-scarecrow.

Discussion · 0

sign in to comment →
No comments yet — be the first.