Troubleshooting
The failures people actually hit, and what each one means.
The page is empty
hasData is false until the first check lands. Nothing checks itself, so
either the scheduler is not running or it has never completed a run.
If that stores results but the page is still empty:
- The page may be statically rendered. Next.js needs
export const dynamic = "force-dynamic"; Astro needsexport const prerender = false. - The snapshot may be cached. It expires after
cache.snapshotTtlMs(default 30 s); callstatusPage.api.invalidate()after an out-of-band write. - Every monitor may be
private: true, which removes it from public snapshots entirely.
Checks never run in production
The local scheduler only exists inside heyo-status dev. Deployments use
platform cron:
| Platform | Check |
|---|---|
| Cloudflare | triggers.crons in wrangler.jsonc matches the three configured expressions, and the Worker exports scheduled. |
| Vercel | vercel.json has the three entries and CRON_SECRET is set. |
| Self-hosted | scheduleStatusPage() is actually called, in a process that stays up. |
Verify manually:
503 The management API is disabled
No admin token. Set adminToken in the configuration or
HEYO_STATUS_API_TOKEN in the environment, then redeploy. On Workers,
process.env may be unavailable — set it as a Worker secret and the runtime
will still find it, or pass adminToken explicitly.
401 Unauthorized
The header must be exactly Authorization: Bearer <token>. Check for a
trailing newline from $(cat token.txt), a quoted value including the
quotes, or a proxy stripping the header.
TLS monitoring is unavailable in this runtime
ssl and tcp monitors need node:tls and node:net:
| Runtime | Works | Note |
|---|---|---|
| Bun 1.4.3+ | yes | |
| Node 20.19+ | yes | Older Node lacks getBuiltinModule. |
| Cloudflare Workers | no | The modules do not exist. |
On Workers, use http and dns, or run certificate checks from a Node host
that writes to the same database.
Cloudflare D1 is not bound
The configuration was imported outside a Worker. A standalone dev server
cannot manufacture a binding — run wrangler dev, or bun run dev in a
generated Cloudflare project, which builds and boots the real Worker.
This is also expected during next build or a CLI command against a
Cloudflare project; the lazy adapter defers it to the request that needs the
database, so the build itself still succeeds.
DATABASE_URL is required
The PostgreSQL overlay resolves its pool on the first query. Set the variable for the environment that is failing — including the one your build or migration job runs in.
heyo-status: command not found
The binary is in the project's node_modules/.bin. Run it through the
package manager — bun run check, pnpm exec heyo-status check run — or
through the generated scripts.
Configuration file not found
The CLI loads ./heyo-status.config.ts by default. Run it from the project
root, pass --config, or set HEYO_STATUS_CONFIG.
heyo-status.config.ts must export a heyoStatus() result
Export it as statusPage, as the default, or both:
Configuration throws on import
heyoStatus() validates with a strict schema, so a misspelled key is an
error rather than a silent no-op. Read the Zod path in the message — it names
the exact field. Common cases:
| Message | Cause |
|---|---|
Unrecognized key(s) | A typo, or an option that moved. |
checks.displayDays cannot exceed checks.retentionDays | The page would read deleted history. |
Composite monitor … references unknown monitor … | A component name does not match a monitor. |
Monitor names must be unique. | Two monitors share a name. |
basePath cannot be "/". | Mount the API below a path segment. |
database must implement the StatusDatabaseAdapter contract. | A partial custom adapter. |
Notifications never arrive
- 1
Check the threshold
monitor.downfires on the run where consecutive failures reachfailureThreshold— not before, and not again while it stays down. - 2
Check the event filter
A channel with
eventsonly receives those types. Remove the filter to test. - 3
Check maintenance
An
in-progresswindow suppresses monitor events for the monitors it names — and for every monitor whenaffectedMonitorsis empty. - 4
Read the log
bashChannel failures are isolated and never surface as a failed check, so the log is the only place the error appears.
Email subscriptions answer 503
POST /subscribers with type: "email" needs a configured email
notification channel — the subscription flow reuses its transport. Add an
email() channel, or drop "email" from subscribers.methods.
target must be a public host
A visitor-supplied webhook pointed at a non-routable address. This is working as intended; see Public subscriptions.
A flash of the wrong colour mode
Render statusThemeBootstrapScript() in the document head and add
suppressHydrationWarning to <html>. Without the script the page paints
before the stored preference is applied.
The subscribe dialog does nothing in Astro
The island needs client:load. Without it, the page renders as inert markup
and neither the dialog nor the colour-mode toggle responds.
Theme changed but the width did not
Make sure you import @heyo-sh/heyo-status/theme/status.css and that the
page renders through StatusShell, which publishes data-theme — the
per-theme tokens are scoped to that attribute.
Duplicate checks or duplicate alerts
More than one scheduler is running: several replicas each calling
scheduleStatusPage(), or a platform cron plus a local dev against the
same database. Exactly one scheduler should own a deployment.