UptimeFlareCloudflare

UptimeFlare

An uptime monitor and status page on one Worker.

uptimeflare

lyc8503's UptimeFlare — an uptime monitor and status page: HTTP, HTTPS and TCP checks every minute, 90 days of uptime bars, twelve hours of latency charts, incident history, scheduled maintenances, webhook notifications and a JSON API — running as one Cloudflare Worker: the status page built by @opennextjs/cloudflare into Workers + Static Assets, and the checks on a Cron Trigger of the same Worker, their state in a Cloudflare D1 database provisioned by Terraform through Orun and its schema applied by migrations — and packaged as an Orun baseline: one command takes a new status page from nothing to live and checking on stage and prod, in its own repository, under its own name, in ONE landing.

Upstream already runs on Cloudflare, as two deployables — a Pages project built by @cloudflare/next-on-pages for the page and a second Worker for the checks, sharing one D1 database. Here they are one Worker: its custom entry (apps/web/worker.ts) re-exports OpenNext's fetch and adds scheduled, which runs upstream's checker loop unchanged. The Durable Object remote checker (geo-specific checks) is left out. What the baseline adds is everything needed to make a copy of it somebody else's, built the way turso-starter and cron-starter are. See specs/epics/uptimeflare for the design, the milestones and the as-built status.

Instantiating a product

git clone --depth 1 --branch <baseline-tag> https://github.com/sourceplane/uptimeflare
cd uptimeflare
orun new --blueprint repo-blueprint.yaml --out ~/sites/acme-status --run-hooks --resume \
  --set reponame=acme-status --set productname="Acme Status" \
  --set githuborg=acme-inc --set subdomain=<workers-dev-subdomain>

Or, once the platform's registry lists it, from the Orun console (Baselines → uptimeflare → Build). Two phases: 01-product creates the repository and lands the whole product as ONE pull request — intent, CI, tooling, the D1 root, the migrations, the status page with its checker, and its tests — and one convergence, per environment, applies the D1 database, then its migrations, then deploys the Worker with its id and its Cron Trigger, smoke-tested on a check the trigger recorded; 02-docs records the live deployment from probed facts. BOOTSTRAP.md is the operator guide; docs/phases/ has a page per phase. The Workers Free plan is enough: the product registers one Cron Trigger per Worker.

The new repository receives the product only — the app, its D1 root, its migrations and their runner, its tests, its CI, its tooling, a README of its own and its operating contract — renamed throughout to the product's name, repository, Cloudflare Worker and database names. None of this repository's machinery ships.

What makes it a baselineWhere
The console card (inputs, secrets, programme)blueprint.yaml
The build document (modules, phases, hooks)repo-blueprint.yaml
This tree's identity, and the bootstrap's task contracts.baseline/
The rename enginetooling/rebrand/rebrand.mjs
The bootstrap's helpers (deferrals, redeploy marker, work-tree strip)tooling/bootstrap/
The component-docs generatortooling/catalog/gen-component-docs.py
The deployment-record rendererhooks/render-deployment-docs.sh
The product's README and context pack.baseline/product/README.md, ai/context/
The baseline's tests: contract, wiring, leak, offline bootstrap, end to endtesting/, .github/workflows/baseline.yml
Cutting a baseline-vN tag.github/workflows/tag.yml

What is here (the product)

SurfaceWhere
The status page, the incidents page, their components and translationsapps/web/pages, apps/web/components, apps/web/locales
The monitors, links, maintenances and notificationsapps/web/uptime.config.ts (every option: uptime.config.full.ts)
The page's name and source linkapps/web/lib/site.ts
The checker: HTTP(S) and TCP checks, incidents, notifications, the compacted stateapps/web/monitor/ (upstream's worker/src)
The Worker entry: OpenNext's fetch + the checker's scheduledapps/web/worker.ts
The JSON API and the status badgeapps/web/pages/api/data.ts, apps/web/pages/api/badge.ts
The password gate (optional)apps/web/proxy.ts
Workers build, trigger, bindings and deploy contractapps/web/next.config.mjs, open-next.config.ts, wrangler.template.jsonc + wiring.fixture.json (→ the committed wrangler.jsonc), component.yaml
The D1 database (stage, prod)infra/terraform/cloudflare-d1
The schema (migrations) and the runner that applies them over the D1 REST APIpackages/db, run by infra/db-migrate
The wrangler renderer, the Worker-secrets helper, the migration helperstooling/wire, tooling/migrations
Teststests/web (the state, the checker against SQLite, the API, the gate), tests/db (the migrations into real SQLite)

Configuring the monitors

Everything a status page shows is in apps/web/uptime.config.ts, as upstream has it: pageConfig (title, header links, groups, logo, footer), workerConfig (the monitors, the write cooldown, notifications, callbacks) and maintenances. It ships with three public monitors — Cloudflare and GitHub over HTTPS, Google Public DNS over TCP — so a new page has something to check; replace them with your own. Each monitor's id keys its history: keep it and the history stays, change it and the monitor starts over.

Monitor kindmethodtarget
HTTP(S), with optional expectedCodes, headers, body, responseKeyword, responseForbiddenKeyword, timeoutGET, HEAD, POST, …a URL
TCP connectTCP_PINGhost:port

A check through a proxy (checkProxy) works for globalping:// and http(s):// proxies; worker://<region>, upstream's Durable Object, is not part of this port — such a monitor reports why it is down, or is checked from the Worker itself with checkProxyFallback: true.

Only id, name, tooltip, statusPageLink and hideLatencyChart reach the browser (the CVE-2026-29779 fix, kept); a test holds that list. A webhook whose URL embeds a token is still a credential in this file, as upstream designs it — keep the repository private, or send notifications from a callbacks function, which receives the Worker's env.

Getting started

pnpm install
pnpm run wire:fixture   # render apps/web/wrangler.jsonc from the offline fixture
pnpm typecheck
pnpm lint
pnpm test
pnpm build              # next build + opennextjs-cloudflare build → apps/web/.open-next
pnpm --filter @app/web db:migrate:local   # create the monitor_state table in wrangler's local D1
pnpm --filter @app/web dev                # http://localhost:3000 — the pages, against that D1 (no scheduler)
pnpm --filter @app/web preview            # wrangler dev over the built Worker, same local D1

Watching a check run

wrangler dev never runs the clock; --test-scheduled adds a /__scheduled endpoint that fires the handler:

cd apps/web
pnpm exec wrangler dev --port 8808 --config wrangler.jsonc --test-scheduled

curl http://127.0.0.1:8808/api/data                     # {"error":"No data available"} — nothing checked yet
curl "http://127.0.0.1:8808/__scheduled?cron=*+*+*+*+*"  # runs the checker once
curl http://127.0.0.1:8808/api/data                     # {"up":…,"down":…,"updatedAt":…,"monitors":{…}}

Deployed, the trigger fires every minute; the state is written when a status changes and otherwise every three minutes (kvWriteCooldownMinutes). Workers → the Worker → Triggers shows the one Cron Trigger; its logs show each check.

A private status page

Store user:password as the product's Orun secret STATUS_PAGE_PASSWORD for an environment and redeploy it: the build hands it to the Worker as a secret (tooling/wire/worker-secrets.mjs, the value over stdin, never in a bundle), and apps/web/proxy.ts asks every request for those credentials, the API's included. Locally, put the same line in apps/web/.dev.vars (.dev.vars.example). Removing the Orun secret does not remove the Worker's copy: delete it with wrangler secret delete STATUS_PAGE_PASSWORD --name &lt;worker> too.

How it maps to UptimeFlare's own deployment

UpstreamHere
A Pages project built by @cloudflare/next-on-pagesThe same pages on Workers + Static Assets, built by @opennextjs/cloudflare (Next 16, pages router); runtime = 'edge' gone — OpenNext runs the Node runtime, and the two API routes are Node API routes
A second Worker (worker/) with a Cron TriggerThe same Worker: worker.ts adds scheduled, which runs the checker (monitor/), on one every-minute trigger
env.UPTIMEFLARE_D1, read through process.env on Pagesenv.DB, read through getCloudflareContext()
The uptimeflare(key, value) table, created by deploy/init_d1.pymonitor_state(key, value), created by the migration 010_monitor_state before the Worker deploys
deploy.tf: the database, the Worker, its trigger, the Pages projectinfra/terraform/cloudflare-d1 for the database; the Worker, its trigger and its assets in one wrangler deploy
The RemoteChecker Durable Object (worker:// geo checks)Left out (no Orun composition places a Durable Object); globalping:// and http(s):// proxies unchanged
workerConfig.passwordProtection, a string in the committed configThe optional Orun secret STATUS_PAGE_PASSWORD, a Worker secret
The page title in uptime.config.tsSITE_NAME in lib/site.ts, which pageConfig.title reads; the server-rendered page carries the title too
GitHub Actions: build, init D1, Terraform applyOrun: one convergence per merge applies the database, migrates it and deploys <repo>-web-{stage,prod}, smoke-tested on a check the trigger recorded

How the database reaches the Worker

infra/terraform/cloudflare-d1 ──apply──► orun secret WIRING_CLOUDFLARE_D1 (stage, prod)
                                               ▼
infra/db-migrate  dependsOn cloudflare-d1 (enforced on main) · secretEnv WIRING_CLOUDFLARE_D1
  apply:  packages/db's runner → POST …/d1/database/<id>/query, each pending migration once
                                               ▼
apps/web  dependsOn cloudflare-d1 + db-migrate (enforced on main) · secretEnv WIRING_CLOUDFLARE_D1_<ENV>
  build:  tooling/wire/live.mjs → wrangler.jsonc with the environment's database id
          (dev, verify-only, renders the fixture) → next build → opennextjs-cloudflare
  deploy: worker.ts (fetch + scheduled) and its one trigger, * * * * *
  smoke:  /api/data carries an updatedAt the trigger's checks wrote, no older than ten minutes

wrangler.jsonc is committed as the fixture render of wrangler.template.jsonc, because the deploy lane's verify-app-structure step checks the config exists before it builds.

CI

WorkflowWhat it runsShips to a product
ci.ymlthe Orun pipeline, and the only workflow a product carries: orun plan --changed, then one job per lane. A pull request runs the verify lanes (the packages type-checked, linted, tested and built, both test suites, the D1 root planned, the Worker built and dry-deployed per environment), except the bootstrap's own landings (orun/BASE-*), where plan alone is the check; a merge applies the D1 database, migrates it, then deploys stage → prod. It plans nothing while intent.yaml names the placeholder workspace ws_CHANGE_ME, as this repository does. A manual run (Orunbase's Deploy button, or Run workflow on main) plans the environment and components it names under the push-to-main trigger, so they deploy as a merge deploys themyes
baseline.ymlthe baseline's own tiers: contract tests, the wiring path, the component docs, the blueprint read by the pinned orun, the leak gate, the offline bootstrap with the product's own plan, and the product built in the job tree, migrated, dry-deployed, served, its trigger fired and its checks read backno
tag.ymlcuts a baseline-vN tag (manual)no

Licence

UptimeFlare is © lyc8503 and its contributors, under the Apache License 2.0 (apps/web/LICENSE); the code here derived from it keeps that licence.