Open-source synthetic monitoring and status pages, running on Cloudflare.
One click. Cloudflare clones this repo to your GitHub, provisions D1 / KV / Queue, and deploys the Worker.
Requires Workers Paid (~$5/month) for Durable Objects and Analytics Engine.
Create HTTP and heartbeat checks in the admin UI, run them from multiple Cloudflare regions, and publish a public page: fully operational / degraded / failing.
This is a GitHub template. Instance data (hosts, monitors, accounts) lives in your D1 after you deploy, never in this repo.
Foxwatch observes from Cloudflare. A global Cloudflare outage takes the observer down with the target. That is an explicit tradeoff, not a bug.
The Deploy to Cloudflare button:
- Clones this repo into your GitHub account
- Provisions D1
foxwatch-data, queuefoxwatch-alerts, and a KV namespace (name itfoxwatch-statuson the setup page) - Injects those IDs into your clone only, deploys the Worker, and turns on Workers Builds
Committed wrangler.jsonc in this template omits D1/KV IDs on purpose. Your account IDs never land in the public repo.
Then open /admin on your Worker and create the first superadmin (email + password).
Until that account exists, anyone who reaches /admin can create it, so do it right after the deploy finishes. To close that window, set a Worker secret named FOXWATCH_SETUP_TOKEN (dashboard Settings → Variables and Secrets, or the command below) before you open /admin. The setup screen then asks for the token and refuses to create the account without it. Leave it unset and setup works as before.
pnpm exec wrangler secret put FOXWATCH_SETUP_TOKENThis repo deploys from Actions on main. Nothing is written into wrangler.jsonc.
Create an API token: Cloudflare dashboard → Manage Account → API Tokens → Create Token → Custom token.
- Token name:
foxwatch-github-actions(or anything you like) - Account resources: Include → the account you will deploy to
- Zone resources: None
Account permissions
| Resource | Access |
|---|---|
| Workers Scripts | Write |
| Workers KV Storage | Write |
| D1 | Write |
| Queues | Write |
| Account Settings | Read |
Copy the token once. Put it in GitHub Settings → Secrets and variables → Actions.
Secret
| Name | Required |
|---|---|
CLOUDFLARE_API_TOKEN |
yes, the token above |
Variables (same page, Variables tab, or secrets with the same names)
| Name | Required |
|---|---|
CLOUDFLARE_ACCOUNT_ID |
yes, account ID |
FOXWATCH_D1_DATABASE_ID |
no, pin D1 foxwatch-data |
FOXWATCH_KV_NAMESPACE_ID |
no, pin KV foxwatch-status |
Without the ID variables, CI runs pnpm foxwatch init --yes (creates or reuses those named resources) then pnpm deploy. IDs are injected into the build output only.
A push with no Cloudflare credentials skips deploy so a fresh template clone stays green. Actions → CI → Run workflow fails instead of silently skipping.
Same setup works if you use this template instead of the button.
pnpm install
pnpm exec wrangler login
pnpm foxwatch init # D1 / KV / Queue → gitignored wrangler.cloud.jsonc
pnpm deployIf foxwatch-data or foxwatch-status already exists, init checks the D1 schema and asks before reusing. It will not migrate over unrelated tables. --yes skips the prompt (CI).
Needs Node 20+ and pnpm. No Cloudflare login.
pnpm install
pnpm dev- Public status: http://localhost:5173/
- Admin: http://localhost:5173/admin
A new instance has no checks and is named Foxwatch until you change it under Settings. Optional: copy .dev.vars.example to .dev.vars for ALLOW_HTTP_LOCAL=true or local header secrets.
All instance data is in D1: operator accounts, site name, HTTP and heartbeat checks, incidents.
Worker secrets for authenticated checks live on the Worker, not in git or D1. Register the name in admin, then:
pnpm exec wrangler secret put YOUR_SECRET_NAMEBy default the admin UI only registers names; values are set with the command above. To set and rotate values from Settings instead, give the Worker these three secrets:
| Name | Value |
|---|---|
FOXWATCH_CF_API_TOKEN |
API token with Workers Scripts write access on the account |
FOXWATCH_CF_ACCOUNT_ID |
the account ID |
FOXWATCH_CF_SCRIPT_NAME |
the Worker's name, foxwatch unless you renamed it |
Values are never read back, in either mode.
Sensitive headers must use a secret ref. Secrets attach only when the request host is in allowedHosts; cross-origin redirects strip them. Only names registered in admin resolve, and Foxwatch's own bindings (FOXWATCH_CF_*, FOXWATCH_SETUP_TOKEN, ALLOW_HTTP_LOCAL) never do. Registering a secret takes a superadmin, and so does any new use of one: other admins can save or test a request that carries a secret only when an existing check already sends that exact request (same URL, method, body, headers and allowed hosts). They can still edit everything else about such a check, and like any admin they see the response snippets of the requests it makes.
Operator accounts - each operator changes their own password under Settings → Your password. That signs them out everywhere else, which is also how to cut off a leaked password or a session left on another machine. Sign-in allows 8 wrong passwords per account from one address (one IPv6 /48) in 15 minutes, and 3 once an account has drawn 200 wrong passwords from anywhere; no number of wrong guesses from elsewhere keeps the right password out. A trip shows in Activity as login-throttled by anonymous, a few per hour at most. Activity entries are kept for 365 days.
If every operator is locked out, empty the accounts table and /admin offers first-account setup again (with the setup token, if you set one):
pnpm exec wrangler d1 execute foxwatch-data --remote --command "DELETE FROM ops_sessions; DELETE FROM ops_users;"Heartbeats - create a check in admin, copy the token (or rotate it):
curl -X POST https://your-worker/api/heartbeat \
-H "Authorization: Bearer <token>"Tokens never go in URLs.
Alert channels - under Settings, add a Slack webhook, Discord webhook or generic webhook and pick the events it gets: fail, degrade, recover. The webhook URL is a Worker secret; the channel stores only its name. Test sends a sample alert. To preview the Discord card from your laptop without a deploy:
pnpm webhook -- --helpMaintenance - start or schedule a window on a component (up to 90 days; windows on one component cannot overlap). While it runs the public page shows the component as under maintenance.
Incidents - a component that fails or degrades opens an incident on its own and resolves it on recovery. Operators can also open one by hand, attach components, and post updates (investigating, identified, monitoring, resolved). Each incident has a public page and appears in the RSS feed.
Status model
- Check: pass / degraded (latency SLO) / fail
- Component: operational / degraded / failing (region quorum, default majority)
- Banner: fully operational / degraded / failing (critical component down)
Public JSON, HTML, RSS, and badges do not include check URLs, headers, or error bodies.
apps/worker- Hono API, public status page, Durable Objects (Scheduler, Monitor, Probe, StatusHub)apps/web- admin UI and the status page globepackages/config- check shape helperspackages/engine- URL policy, assertions, quorum, redactionmigrations- D1 schemascripts-pnpm foxwatch init,pnpm deploy,pnpm webhook
pnpm test, pnpm check-types and pnpm build are what CI runs.
AGPL-3.0-or-later.