Cloudflare Worker + Hono status page with scheduled checks and Drizzle history on D1.
Set STATUS_ENDPOINTS_JSON to a JSON array:
[
{
"name": "API health",
"description": "Core backend API.",
"private": false,
"type": "http",
"url": "https://api.example.com/health",
"method": "GET",
"headers": {
"Authorization": "Bearer example-token",
"Auth-Version": "2024-06-01"
},
"expectedStatus": [200],
"timeoutMs": 10000,
"softFail": true,
"softFailMilliseconds": 500,
"expectedBodyIncludes": "ok",
"expectedBodyExcludes": "error",
"expectedJson": { "status": "ok" }
},
{
"name": "SSL cert",
"type": "ssl",
"host": "example.com",
"port": 443,
"warnBeforeDays": 14
},
{
"name": "Postgres",
"private": true,
"type": "tcp",
"host": "db.example.com",
"port": 5432,
"timeoutMs": 10000
},
{
"name": "DNS example.com",
"type": "dns",
"host": "example.com",
"recordType": "A"
}
]Endpoint can set "private": true to run checks in the background without showing that monitor on / or /api/status. The default is false.
HTTP endpoints can set "softFail": true to retry a failed check after "softFailMilliseconds" before recording it as down. The defaults are false and 500.
HTTP endpoints can set "headers" to send custom request headers such as Authorization or Auth-Version.
Other env vars:
LOGO_URL- logo shown in the top-left header.FAVICON_URL- favicon URL rendered as<link rel="icon">.RETENTION_DAYS- how many days to keep in storage, default90.MOCK_PREVIOUS_DAYS- optional success percentage for bootstrapping history over the previousRETENTION_DAYSdays when there is no stored raw data or daily summaries yet, for example99,9832423.DISPLAY_DAYS- how many days to display on the status page and in/api/status, default60.PAGE_TITLE- page title and header label, defaultStatus Page.FOOTER_TITLE- footer label, defaultParagraph CMS Open Status Page.META- JSON array of{ "name": string, "value": string }entries.og:*entries render as<meta property="...">; all other entries render as<meta name="...">.og:imageandtwitter:imagedefault to/paragraphcms-open-soruce-status-page.jpg, resolved against the current request origin.SUMMARY_CRON- cron expression used to roll up every previous UTC day intostatus_daily_summaries, default10 0 * * *.CLEANUP_CRON- cron expression used to identify the cleanup run, default0 3 * * *.CHECKS_CRON- local/Docker cron expression for status checks, default*/5 * * * *. Wrangler deploys use the cron trigger inwrangler.jsonc.SLACK_WEBHOOK_URL- incoming Slack webhook used after each checks run and byGET /api/slack-statuswhen consecutive failures reach the threshold.SLACK_STATUS_CHECK_COUNT- how many consecutive failed checks per configured monitor must be recorded before Slack is notified, default3.
META example:
[
{
"name": "description",
"value": "Live uptime, availability, and incident status for Paragraph CMS services."
},
{
"name": "og:description",
"value": "Live uptime, availability, and incident status for Paragraph CMS services."
},
{
"name": "og:title",
"value": "Paragraph CMS Status Page"
},
{
"name": "og:type",
"value": "website"
},
{
"name": "og:site_name",
"value": "Paragraph CMS Status"
},
{
"name": "twitter:card",
"value": "summary_large_image"
},
{
"name": "twitter:title",
"value": "Paragraph CMS Status Page"
},
{
"name": "twitter:description",
"value": "Live uptime, availability, and incident status for Paragraph CMS services."
},
{
"name": "og:url",
"value": "https://status.paragraphcms.com/"
}
]Override the default social preview image by adding explicit entries to META:
[
{
"name": "og:image",
"value": "https://paragraphcms.com/branding/paragraphcms-logo-color.svg"
},
{
"name": "twitter:image",
"value": "https://paragraphcms.com/branding/paragraphcms-logo-color.svg"
}
]- Create D1 and paste its
database_idintowrangler.jsonc. - Apply migrations:
bun run db:migrate:remote- Run locally with Wrangler:
bun run dev- If you use Slack notifications, store the webhook as a secret:
bunx wrangler secret put SLACK_WEBHOOK_URL- Deploy:
bunx wrangler deployThe default cron setup runs checks every 5 minutes, summarizes previous days daily at 00:10 UTC, and runs cleanup daily at 03:00 UTC.
When an older deployment already has historical rows in status_results but no status_daily_summaries yet, the first page/API request or the next checks cron run will backfill those summaries automatically and delete the summarized raw rows. You do not need to call the summary endpoint manually after deploy.
bun run dev:localExample local run with env vars:
PORT=3000 \
DATABASE_PATH=/tmp/status-page.sqlite \
PAGE_TITLE='Paragraph CMS Status Page' \
LOGO_URL='https://paragraphcms.com/paragraph-cms-logo.svg' \
FAVICON_URL='https://paragraphcms.com/favicon.ico' \
RETENTION_DAYS=90 \
MOCK_PREVIOUS_DAYS=99,9832423 \
DISPLAY_DAYS=60 \
FOOTER_TITLE='Paragraph CMS Open Status Page' \
SUMMARY_CRON='10 0 * * *' \
SLACK_STATUS_CHECK_COUNT=3 \
CHECKS_CRON='*/5 * * * *' \
SLACK_WEBHOOK_URL='https://hooks.slack.com/services/REPLACE/ME' \
STATUS_ENDPOINTS_JSON='[{"name":"DNS example.com","type":"dns","host":"example.com","recordType":"A"}]' \
bun run start:localDocker uses Bun SQLite through the same Drizzle schema, runs the local cron scheduler, and serves bundled files from assets/ at the site root to match Cloudflare Workers assets:
docker build -t status-page .
docker run -p 3000:3000 \
-e STATUS_ENDPOINTS_JSON='[{"name":"DNS example.com","type":"dns","host":"example.com","recordType":"A"}]' \
status-pageManual endpoints:
GET /- SSR status page.GET /robots.txt- generated robots policy with aSitemapentry for the current origin.GET /sitemap.xml- generated sitemap for the status page homepage.GET /api/status- JSON status snapshot.GET /api/checks/runandPOST /api/checks/run- run all checks, persist results, and notify Slack when the latestSLACK_STATUS_CHECK_COUNTchecks for a monitor are consecutive failures.GET /api/slack-status- read the latestSLACK_STATUS_CHECK_COUNTstored results per monitor and notify Slack throughSLACK_WEBHOOK_URLwhen all inspected results for a monitor are consecutive failures.GET /api/summaries/runandPOST /api/summaries/run- summarize every UTC day before today intostatus_daily_summariesand delete the summarized raw rows.POST /api/cleanup- delete rows older thanRETENTION_DAYS.
Local curl examples:
curl -sS -i http://localhost:3000/healthz
curl -sS -i http://localhost:3000/api/status
curl -sS -i -X POST http://localhost:3000/api/checks/run
curl -sS -i http://localhost:3000/api/checks/run
curl -sS -i http://localhost:3000/api/slack-status
curl -sS -i -X POST http://localhost:3000/api/summaries/run
curl -sS -i -X POST http://localhost:3000/api/cleanup