1. Documentation
  2. Databases
  3. Cloudflare D1
ReadmenpmGitHub
  • Introduction
  • Quickstart
  • Configuration file
  • Monitors
  • Checks and scheduling
  • Notifications
  • Incidents and maintenance
  • Subscribers
  • Metrics and badges
  • Identity and appearance
  • Telemetry
  • The check run
  • Uptime and history
  • Automatic incidents
  • Notification delivery
  • Public subscriptions
  • StatusPage component
  • Themes
  • UI primitives
  • Colour mode
  • React Router
  • Next.js
  • Astro
  • SQLite
  • Cloudflare D1
  • PostgreSQL
  • Schema and migrations
  • Cloudflare Workers
  • Vercel
  • Self-hosting
  • API overview
  • Status endpoints
  • Incident and maintenance endpoints
  • Subscriber endpoints
  • Badges, feeds and metrics
  • Admin endpoints
  • CLI commands
  • Package exports
  • Security
  • Troubleshooting

Cloudflare D1

Bind D1, resolve it lazily and apply migrations with wrangler.

D1 is the database for Cloudflare Workers deployments. A binding only exists inside a Worker request, so the adapter is always wrapped in lazyStatusDatabase() — the configuration module must stay importable in a build step, a CLI command and a test.

heyo-status.database.ts
import { lazyStatusDatabase } from "@heyo-sh/heyo-status/db/lazy";import { d1, type D1Database } from "@heyo-sh/heyo-status/deployments/cloudflare";export const database = lazyStatusDatabase(() => {  const binding = (globalThis as typeof globalThis & { HEYO_STATUS_D1?: D1Database })    .HEYO_STATUS_D1;  if (!binding) {    throw new Error(      "Cloudflare D1 is not bound. Run this project with wrangler; a standalone dev server has no Cloudflare binding.",    );  }  return d1(binding);});

The Worker publishes the binding before the framework handles the request:

src/status-worker.ts
export default {  fetch(request: Request, env: Env, context: ExecutionContext) {    (globalThis as typeof globalThis & { HEYO_STATUS_D1?: D1Database }).HEYO_STATUS_D1 = env.DB;    return fetchHandler({ request, env, /* … */ });  },  async scheduled(event, env, context) {    (globalThis as typeof globalThis & { HEYO_STATUS_D1?: D1Database }).HEYO_STATUS_D1 = env.DB;    const { statusPage } = await import("../heyo-status.config");    return cloudflareWorker(statusPage).scheduled(event, env, context);  },};
There is no local fallback on purpose

A standalone dev server cannot manufacture a Cloudflare binding, and silently writing somewhere else would hide the fact that the deployed database was never touched. Use wrangler dev for a local D1.

Why lazy

Status configuration is a module-level value. Without deferral, a missing binding throws while the module is being imported, which takes down the whole framework process — including pages that never touch the database — and makes every template import order-sensitive. lazyStatusDatabase() moves the failure to the request that actually needs persistence, where it is reported as a normal API error.

A failed resolution is not cached: a missing binding fixed by a redeploy works on the next request, without a restart.

Binding it

wrangler.jsonc
{  "name": "my-status-page",  "main": "src/status-worker.ts",  "compatibility_date": "2026-01-01",  "compatibility_flags": ["nodejs_compat"],  "assets": { "directory": "build/client", "binding": "ASSETS" },  "d1_databases": [    {      "binding": "DB",      "database_name": "my-status-page",      "database_id": "replace-me",      "migrations_dir": "migrations",    },  ],  "triggers": { "crons": ["*/5 * * * *", "10 0 * * *", "0 3 * * *"] },}
bash
wrangler d1 create my-status-page   # copy the id into wrangler.jsonc

Migrations

The generated project copies the bundled SQL into migrations/ and points migrations_dir at it, so wrangler owns the migration state:

bash
wrangler d1 migrations apply my-status-page --localwrangler d1 migrations apply my-status-page --remote

D1StatusDatabase can also migrate itself, which is what a one-off deployment job would use:

ts
await database.migrate();

Request handlers never migrate automatically. d1(binding, { migrate: true }) exists for local and test databases; leave it off for a remote one.

Options

ts
d1(env.DB);d1(env.DB, { migrate: true });
bindingD1Databaserequired

A structural type — importing Heyo Status does not require Workers types.

migratebooleanoptional

Defaults to false. Applies the bundled migrations on construction.

Inserts use binding.batch() when it is available, falling back to parallel statements otherwise.

Limitations on Workers

  • node:net and node:tls are unavailable, so tcp and ssl monitors record … monitoring is unavailable in this runtime. and report down. Use http and dns, which work everywhere.
  • The snapshot cache is per-isolate. That is fine — it still absorbs bursts — but it is not a shared cache.
  • D1 has per-query and per-invocation limits; the default 90-day window with compaction keeps the snapshot query well inside them.

Related

  • Deploying to Cloudflare
  • Schema and migrations
  • Troubleshooting
SQLite< PreviousPostgreSQLNext >

Powered by heyo

On this page

Why lazyBinding itMigrationsOptionsLimitations on Workers