cloudflare-uptime-oss

agent
Security Audit
Warn
Health Warn
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 5 GitHub stars
Code Warn
  • network request — Outbound network request in src/alerts.ts
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Self-hosted uptime monitoring on Cloudflare Workers with public status pages

README.md

cloudflare-uptime

MIT License
Cloudflare Workers
Deploy
AGENTS.md

Self-hosted uptime monitoring on Cloudflare Workers with public status pages — no servers, no monthly fees.


Table of Contents


Features

  • Multi-monitor support — track any HTTP endpoint or TCP socket (via tcp://) with configurable check intervals and timeouts
  • Public status pages — shareable /status/:slug pages with live up/down status per monitor
  • 90-day latency history — sparkline graph built from the rolling check history; can be hidden per status page from the admin dashboard if you'd rather not publish response-time data
  • Incident timeline — timestamped incidents with human-readable failure reasons (HTTP status badge + description, timeout label, or raw error)
  • Incident history page/status/:slug/history lists all incidents grouped by month with collapsible sections; history window (30 or 90 days) is configurable per status page from the admin dashboard
  • Minor incident filtering — hide resolved incidents shorter than a configurable duration (5/15/30/60 min) from public status pages, the history page, and the RSS feed, so transient blips don't clutter real outages; ongoing incidents are always shown
  • RSS feed/status/:slug/rss for incident subscribers
  • Custom logo per page — (Optional) upload a logo to R2; served through the Worker with immutable cache headers
  • Branded header templates — choose a Default (centered), Banner, Compact, or Navbar header layout per status page, and set a brand_color (validated hex) that flows through the header background/underline and site-wide accent links; unbranded pages render exactly as before. Banner and Navbar backgrounds span the full page width; Navbar also shows a live "next check" countdown and an RSS bell icon, with the overall status summary floating over its bottom edge
  • Editable status page description — set or update the description shown under the page title at any time from the admin dashboard, not just at creation
  • Custom domain routing — each status page can be served on its own domain via wrangler.toml routes
  • Expected status code — configure the expected HTTP response code per monitor (useful for endpoints that intentionally return 201, 204, 301, or any non-200 status)
  • JSON payload monitoring — extract a field from the JSON response body and map its value to up, degraded, or down; built-in Statuspage.io preset covers Anthropic, GitHub, and any other Statuspage.io-powered status page out of the box
  • Degraded state — yellow third state between green and red; degraded monitors appear on the status page without opening incidents or firing alerts
  • Admin dashboard — add/edit/delete monitors and status pages, view check history with extracted JSON values per check; "Load more" pages back through a monitor's full 90-day retention window
  • Dark mode — full light / dark / system theme support across the admin dashboard, status pages, and history pages; toggle persists via localStorage
  • Slack and Discord webhook alerts — per-monitor webhooks fire on incident open and close
  • Maintenance notices — create notices that appear on status pages; resolved notices stay visible for 24 hours with a "Resolved" badge
  • Cron-based checks — Cloudflare cron triggers run the check loop on your configured schedule; per-monitor check timing is staggered by a deterministic offset so monitors sharing a common-multiple interval (e.g. 1/5/10/30 minutes) don't all land in the same tick
  • Self-monitoring health check — a separate, low-frequency cron watches for monitors that have stopped reporting on schedule (a sign the check loop itself isn't completing) and shows a dashboard banner; optionally fires a Slack/Discord webhook on state changes (HEALTH_ALERT_WEBHOOK)
  • R2 asset storage — logo uploads stored in and served from Cloudflare R2
  • CI/CD via GitHub Actions — add CLOUDFLARE_API_TOKEN as a repo secret and trigger deploys manually (or change the workflow trigger to auto-deploy on push)
  • Security scanning — Trivy (dependency CVEs) and Gitleaks (secret detection) run on every push

Screenshots

drop-kit-landscape-1779790200368 drop-kit-landscape-1779791596122 drop-kit-youtube-banner-1785333964449 drop-kit-youtube-banner-1785333944471

Prerequisites

  • Node.js 20+ (Node 24 recommended — matches the CI workflow)
  • Wrangler CLI — installed via npm install (listed as a dev dependency)
  • A Cloudflare account (free tier is sufficient)
  • A Cloudflare API token with Workers:Edit, D1:Edit, and R2:Edit permissions

Quick Start

git clone https://github.com/ANDRS-Projects/cloudflare-uptime.git
cd cloudflare-uptime
./setup.sh

setup.sh walks through every step interactively. For a manual walkthrough see the full Deployment Guide below.


Deployment Guide

1. Install dependencies

npm install

2. Authenticate with Cloudflare

npx wrangler login

This opens a browser window. Alternatively, set CLOUDFLARE_API_TOKEN in your shell environment.

3. Create the D1 database

npx wrangler d1 create uptime-monitor

The output includes a database_id. Copy it, then open wrangler.toml and paste it here:

[[d1_databases]]
binding = "DB"
database_name = "uptime-monitor"
database_id = "PASTE_YOUR_DATABASE_ID_HERE"

4. Apply the database schema

# Local dev database:
npm run db:init

# Remote (production) database:
npm run db:init:remote

Important: wrangler deploy does NOT apply schema.sql automatically. You must run this step manually on first deploy. For subsequent schema changes, run ALTER TABLE statements directly via the Cloudflare D1 Console.

5. Create the R2 bucket (Optional)

R2 storage is only required if you want to upload custom logos for your status pages. If you skip this, the logo upload feature will be automatically disabled in the admin dashboard.

npx wrangler r2 bucket create uptime-assets

If you choose a different bucket name, update wrangler.toml:

[[r2_buckets]]
binding = "ASSETS"
bucket_name = "your-bucket-name"

6. Set the API key secret

npx wrangler secret put API_KEY

Enter a strong random string when prompted. Generate one with:

openssl rand -hex 32

This secret is the only authentication mechanism for the admin dashboard and all /api/* routes. Guard it carefully.

7. Deploy

npm run deploy

Your Worker is now live at https://cloudflare-uptime.<your-subdomain>.workers.dev.

8. (Optional) Custom domain routing

To serve a status page on status.yourdomain.com, add an entry to the routes array in wrangler.toml:

routes = [
  { pattern = "status.yourdomain.com", custom_domain = true },
]

You can add as many subdomains as you like — each status page can have its own domain. Then redeploy with npm run deploy.

Do not create DNS records manually. Wrangler creates and manages the DNS record for you automatically on deploy. If you pre-create a record, the deploy will fail with a code: 100117 error — delete the manually created record in the Cloudflare dashboard first, then redeploy.

Always commit wrangler.toml before deploying via CI. If you add a domain and run npm run deploy locally without committing, the domain will work — but the next CI deploy will use the committed wrangler.toml (without your new route) and Wrangler will remove the DNS record, causing a 1016 Origin DNS error. Commit and push wrangler.toml changes alongside any local deploy.

The domain's zone must be on Cloudflare DNS (i.e. your domain uses Cloudflare nameservers). If your domain uses external nameservers, point a CNAME at your .workers.dev URL instead and omit custom_domain = true.

9. (Optional) Set up CI/CD

Add your Cloudflare API token as a GitHub Actions secret named CLOUDFLARE_API_TOKEN.

The included .github/workflows/deploy.yml is set to manual trigger by default (workflow_dispatch) so it won't fail on forks before secrets are configured. To enable auto-deploy on every push to main, change the on: trigger in the workflow file to:

on:
  push:
    branches: [main]

Then trigger a deploy from the Actions tab → DeployRun workflow.


Configuration Reference

All configuration lives in wrangler.toml (infrastructure) or as Wrangler secrets (runtime).
There is no traditional .env file — see .env.example for a full annotated reference.

wrangler.toml fields

Field Default Description
name cloudflare-uptime Worker name as shown in the Cloudflare dashboard
main src/worker.ts Entry point — do not change
compatibility_date 2024-12-01 Workers runtime version pin
workers_dev true Enables the .workers.dev subdomain
d1_databases[].database_id YOUR_D1_DATABASE_ID Paste output from wrangler d1 create
d1_databases[].binding DB Binding name — referenced as Env.DB in src/types.ts
r2_buckets[].bucket_name uptime-assets R2 bucket for logo uploads
r2_buckets[].binding ASSETS Binding name — referenced as Env.ASSETS in src/types.ts
triggers.crons ["* * * * *"] Cron schedule for check runs (every minute by default)
routes[].pattern Custom domain (e.g. status.yourdomain.com)
routes[].custom_domain true Required for Cloudflare-proxied domains

Wrangler secrets (runtime)

Secret Required Description
API_KEY Yes Authenticates all /api/* requests via the X-API-Key header
HEALTH_ALERT_WEBHOOK No Slack/Discord webhook for self-monitoring alerts — fires when monitor checks stop landing on schedule (see src/health.ts)

GitHub Actions secrets (CI/CD)

Secret Required Description
CLOUDFLARE_API_TOKEN Yes Needs Workers:Edit, D1:Edit, R2:Edit permissions

Per-monitor settings (set via admin dashboard)

Field Description
url The HTTP(S) endpoint or TCP socket (e.g., tcp://example.com:5432) to monitor
interval_minutes How often to check (1, 5, 10, 15, 30, 60)
timeout_ms Request timeout in milliseconds (default: 10000)
expected_status_code Expected HTTP response code (optional — leave blank to accept any 2xx–3xx)
retry_count Total check attempts before marking a site down (default: 3 — each failed attempt waits 2 s before retrying, so 3 attempts = up to 4 s before an incident fires)
alert_webhook Slack or Discord incoming webhook URL for up/down alerts
json_path Dot-notation path to extract from the JSON response body (e.g. status.indicator). Leave blank for plain HTTP monitoring.
json_status_map JSON object mapping extracted values to up, degraded, or down (e.g. {"none":"up","minor":"degraded","critical":"down"}). Select the Statuspage.io preset in the admin to fill this automatically.

Per-status-page settings (set via admin dashboard)

Field Default Description
incident_history_days 30 Number of days shown on the /history page — set to 30 or 90
min_incident_duration_minutes 0 Hide resolved incidents shorter than this from public views (0 = show all); set to 5, 15, 30, or 60
header_template centered Status page header layout — centered (default), banner (full-bleed brand-colored block), compact (inline header with a brand-colored underline), or navbar (full-bleed dark bar with live status countdown and RSS bell; the overall status summary floats over its bottom edge, above any notices)
brand_color (none) Hex color (e.g. #7c3aed) applied to the header background/underline and site-wide accent links (RSS/history links); leave unset to use the default theme accent. Does not affect up/down/degraded status colors.

Development

npm run dev       # Start local dev server (wrangler dev, binds to http://localhost:8787)
npm run db:init   # Apply schema to local D1 (creates .wrangler/state/)

The admin dashboard is at http://localhost:8787/. Type-check without deploying:

npx tsc --noEmit

Code style:

  • Strict TypeScript ("strict": true in tsconfig.json)
  • No frontend build step — all HTML is inline template literals in src/html/
  • No external runtime dependencies beyond hono
  • noUnusedLocals and noUnusedParameters are enforced by the compiler

Caveats

D1 schema migrations are not automatic.
wrangler deploy does not run schema.sql. Apply the schema manually with
npm run db:init:remote on first deploy, and run ALTER TABLE statements
via the Cloudflare D1 Console for any subsequent schema changes.

Upgrading from an earlier version requires the following manual ALTER TABLE statements in the D1 Console (safe to run on existing data — all columns have defaults):

ALTER TABLE incidents ADD COLUMN trigger_status_code INTEGER;
ALTER TABLE incidents ADD COLUMN trigger_error TEXT;
ALTER TABLE status_pages ADD COLUMN incident_history_days INTEGER NOT NULL DEFAULT 30;
ALTER TABLE monitors ADD COLUMN expected_status_code INTEGER;
ALTER TABLE monitors ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 3;
ALTER TABLE monitors ADD COLUMN json_path TEXT;
ALTER TABLE monitors ADD COLUMN json_status_map TEXT;
ALTER TABLE checks ADD COLUMN degraded INTEGER NOT NULL DEFAULT 0;
ALTER TABLE checks ADD COLUMN json_value TEXT;
ALTER TABLE status_pages ADD COLUMN min_incident_duration_minutes INTEGER NOT NULL DEFAULT 0;
ALTER TABLE status_pages ADD COLUMN header_template TEXT NOT NULL DEFAULT 'centered';
ALTER TABLE status_pages ADD COLUMN brand_color TEXT;
ALTER TABLE status_pages ADD COLUMN show_latency INTEGER NOT NULL DEFAULT 1;

Migration files for each schema change are kept in migrations/ and can be applied with:

npx wrangler d1 migrations apply uptime-monitor --remote

Run migrations before you deploy, not after. The Worker's cron job writes to any table added by a migration on every check — if you deploy new code before applying its migration, cron starts erroring on that write until you catch up (harmless — it just means a gap in that table until you apply it — but worth avoiding). deploy.yml in this repo is manual-trigger by default (workflow_dispatch), so you're in control of the order either way: run npx wrangler d1 migrations apply uptime-monitor --remote first, then deploy. A fresh install doesn't need this — schema.sql already includes every table, and setup.sh / step 4 of the Deployment Guide applies it before the deploy step.

Migration 007_add_uptime_bucket_rollups.sql (added in 1.6.2) is the one migration here that isn't a plain ALTER TABLE — it creates a new uptime_bucket_rollups table and backfills it from your existing checks data in the same statement, so the uptime bar and uptime% don't show a gap for the 30 days before you upgraded. Apply it the same way: npx wrangler d1 migrations apply uptime-monitor --remote.

Migration 008_add_show_latency.sql adds a per-status-page toggle for the response time graph (show_latency, defaults to on — existing pages keep showing it unless you turn it off from the admin dashboard).

Migration 009_add_worker_health.sql adds the worker_health table backing the self-monitoring health check. Unlike the other migrations, this one also needs a wrangler.toml change that isn't part of the migration itself: add a second cron trigger so the health check runs independently of the 1-minute check loop —

[triggers]
crons = ["* * * * *", "*/15 * * * *"]

Apply the migration before deploying the updated code, same rule as above — otherwise the health-check cron errors on every tick until the table exists.

Custom domains require Cloudflare DNS — and no pre-created DNS records.
custom_domain = true in wrangler.toml only works when the domain's zone is on Cloudflare DNS.
Wrangler creates the DNS record automatically on deploy — do not create it manually first or the
deploy will fail with code: 100117. For domains on external nameservers, use a CNAME pointing
to your .workers.dev URL and omit custom_domain.

Cron triggers run from a single datacenter.
Cloudflare cron triggers fire from the datacenter nearest to your D1 region — not
from multiple global locations. Check latency results reflect that single origin's network path.

Public status pages can lag up to 65 seconds behind the latest check or incident update.
Both public data endpoints (/status/:slug/data, /status/:slug/history/data) sit behind a
65-second Workers Cache API layer — just over the page's 60-second client-side auto-refresh
interval, so a visitor's own refresh loop can actually land on a cache hit instead of missing
every single time. The underlying data can't change faster than the cron interval (1 minute
minimum) anyway, and status pages can have many concurrent viewers. A new check result, resolved
incident, or notice may take up to 65s to appear on the public page — the admin dashboard is not
cached and always reflects live data.

The admin dashboard has no authentication UI.
All admin API routes require an X-API-Key header matching the API_KEY secret. The
dashboard reads this key from a value you enter in the browser. Do not expose the Worker
URL without this protection.

Some targets rate-limit Cloudflare shared IPs.
GitHub Pages and several CDNs throttle requests from Cloudflare's shared IP ranges at
short intervals. Use 5-minute or longer check intervals (interval_minutes >= 5) for
those targets to avoid false positives from rate-limit responses (HTTP 429).


Using with Claude Code

This project includes a CLAUDE.md that gives Claude Code complete context: commands,
architecture, key files, and gotchas specific to the Workers + D1 runtime.

claude    # Start Claude Code — reads CLAUDE.md automatically

Contributing

See CONTRIBUTING.md for the fork-and-PR workflow, code style guidelines,
and how to report issues or vulnerabilities.


License

MIT — see LICENSE.

Reviews (0)

No results found