Skip to content
DocsDocumentation

Production checklist

Everything to verify before a Lunora app takes real traffic — bindings, secrets, admin tokens, rate limits, RLS, migrations, observability.

Last updated:

A pre-deploy checklist for taking a Lunora app to production. Each section links to the page that covers the topic in depth; this page is the gate, not the manual. The mechanics of deploying (wrangler config, the deploy flow, containers, log drains) live in Deployment.

Bindings and wrangler config

  • wrangler.jsonc declares the Durable Object bindings (SHARD, and SCHEDULER if you schedule work), the D1 database for .global() tables, and the R2 bucket for file storage; see the reference config in Deployment.
  • The migrations block lists your DO classes under new_sqlite_classes (Durable Object class migrations are wrangler config, not SQL).
  • lunora verify passes. It validates wrangler.jsonc, dry-runs codegen, and runs tsc --noEmit without writing files; a stale _generated/ or a mistyped binding fails here instead of at runtime.
  • lunora doctor is clean: it preflights the project's wrangler bindings, placeholder values, and dev secrets. lunora doctor --fix creates the D1 databases the config names (reusing any that already exist) and writes their ids in, and mints the local .dev.vars secrets. It refuses to write .dev.vars unless git ignores it. --fix --dry-run prints the same changes without making them. R2 buckets and queues are not created by it.
  • You know what keeps a Durable Object awake. From compatibility_date 2026-10-01 (flag durable_object_io_tasks_prevent_eviction), a pending timer, waitUntil or outbound call blocks idle eviction for up to 15 minutes per operation, and that time is billed. Lunora's own shards arm timers only while a live stream is open. A LunoraClient or an EventsSync poll created inside a Durable Object keeps heartbeat and poll timers armed while idle, so that Durable Object never evicts. Run such clients in a Worker, or opt out with durable_object_io_tasks_do_not_prevent_eviction.
  • A single-page app calls client.close() when it tears the client down. Unsubscribing every query releases a shard's socket after a short idle window, but the default shard's socket is the client's home connection and stays open (with its heartbeat) until close() or page unload.

Secrets

Local secrets live in .dev.vars (gitignored); production secrets are Cloudflare Worker secrets, set per environment. wrangler deploy never pushes .dev.vars values. Full flow in Deployment → Secrets.

  • Every key in .dev.vars.example has a production value: wrangler secret put NAME --env production, or push in bulk with lunora env push --prod.
  • LUNORA_RELAY_SECRET is set when you run more than one shard. It signs the internal control channel between shards (relay and read replicas). Without it the channel trusts any caller inside the Durable Object namespace.
  • Generated secrets are strong. lunora env generate mints 32-byte-hex values for everything locally generatable; provider keys (Resend, Stripe, …) come from the provider dashboard.
  • lunora env doctor exits zero; it checks .dev.vars against the example for missing keys, still-unset placeholders, and stray extras, and works as a CI gate.
  • If you use Cloudflare Secrets Store bindings, functions read them with ctx.secrets.get(name); confirm the store binding exists in the target environment.
  • CI deploys rely on the built-in guard: a non-interactive lunora deploy aborts when a required secret is missing on the target worker.

LUNORA_ADMIN_TOKEN and the admin plane

The admin token gates every /_lunora/admin/* route plus /_lunora/migrate: data export/import, migrations, the Studio, scheduled-job admin.

  • LUNORA_ADMIN_TOKEN is set as a production secret and is at least 24 characters (the admin-token-weak security advisor flags shorter ones; see Security).
  • It is stored only in your secret manager/CI, never in wrangler.jsonc vars or the repo. Rotating it also invalidates every outstanding ephemeral WS sub-token, so rotation is your kill switch.
  • Ephemeral WS tokens stay enforced. A browser WebSocket cannot set an Authorization header, so admin sockets authenticate via ?token=, which lands in access logs and browser history. Instead of the raw master token, the worker mints a short-lived (60s) HMAC-signed sub-token via POST /_lunora/admin/ws-token (itself gated by the master token in the Authorization header). Enforcement is on by default: the WS admin gate (worker and Durable Objects both) rejects the raw master token in ?token=; only minted sub-tokens (or the master token in the Authorization header, which never leaks via URLs) authorize. The Studio mints sub-tokens itself and never falls back to the master token. There is no opt-out. The sub-token itself still rides the URL for up to 60 seconds, so proxy and access logs can record it.

Rate limiting and abuse

  • Public mutations that create users, send mail, or consume credits are wrapped in @lunora/ratelimit middleware; the public_mutation_without_ratelimit advisor lint finds the gaps.
  • Consider protectPublic({ rateLimit, captcha }) from @lunora/server to chain a rate limit and a CAPTCHA check onto a public procedure in one .use(); see Middleware.

Auth and row-level security

  • Identity is wired: resolveIdentity (or @lunora/auth's handler) stamps ctx.auth on every call; see Authentication.
  • authAdmin is set if you use the Studio's auth panel (the deprecated authIntrospector option is gone; see the upgrade guide).
  • Tables holding user data have RLS policies, and fields that must never reach other users are masked.
  • The secure-by-default edge (CORS allowlist, CSRF guard, security headers) is not switched off in production; the deployment-level security advisors (cors-wildcard-credentials, security-headers-disabled, csrf-disabled, …) audit the live worker. See Security.

Migrations

Lunora has two migration surfaces; see Migrations.

  • D1 schema migrations for .global() tables are generated (lunora migrate generate) and committed as a review artifact. Nothing applies them: the runtime provisions global tables from the schema on first use (CREATE TABLE IF NOT EXISTS + additive ADD COLUMN), so any hand-written SQL in a generated file must be applied yourself (wrangler d1 execute <DB> --remote --file=…) before the deploy.
  • Data migrations (lunora migrate create --table …, run with lunora migrate up) have been dry-run against a copy where the change is destructive. Runs are admin-gated: set LUNORA_ADMIN_TOKEN or pass --token.
  • DO class changes are reflected in the migrations block of wrangler.jsonc.

Observability

  • An observability sink is configured on createWorker: otlpSink to a collector, webhookSink to an error tracker (Sentry, PostHog, …), analyticsEngineSink, or several via combineSinks. Without one, a deployed worker's telemetry goes nowhere. See Observability.
  • You know where errors surface: the Studio groups error events into Issues: deterministic fingerprinting collapses the same error across live events and request-log rows into one issue.
  • If the worker sits behind a gateway or mesh that sets traceparent itself and you want end-to-end waterfalls, trustInboundTraceContext is set. Leave it off on anything an untrusted client can reach directly: the header is caller-supplied, so honouring it lets a client pick which trace its spans and logs join. See Joining a trace from upstream.
  • Log access is sorted: lunora logs tails a deployed worker live; durable retention needs a tail_consumers Worker or "logpush": true. See Deployment → Streaming logs.

Health and readiness endpoint

The worker exposes two public probe routes an uptime monitor or load balancer can poll. They return a small JSON body (a status, per-check up/down, and static app metadata) and never leak a secret, env value, connection string, or binding name.

  • GET /_lunora/health is the aggregate probe. It runs every registered check and returns 200 when all critical dependencies (the Durable Object namespace, D1) are up, 503 when any critical dependency is down. A non-critical failure (a presence gap on R2, a queue) degrades the reported status to "degraded" but keeps the 200, since the deployment still serves traffic. The body's status is one of "healthy", "degraded", or "unhealthy".
  • GET /_lunora/health/ready is the readiness gate. It runs only the readiness checks and returns 503 whenever any is unhealthy, so a load balancer stops routing to a not-yet-ready instance.

Checklist:

  • Point an uptime monitor or a Cloudflare Health Check at /_lunora/health (open in dashboard: Traffic → Health Checks) so a critical-binding outage pages you. Use /_lunora/health/ready for a load balancer's origin health / readiness pool.
  • Decide the auth posture. The default is "public": unauthenticated, message-redacted, and name-redacted: each check surfaces only its probe kind (d1, r2, queue, …, with a #2 suffix if a kind repeats) plus up/down, so the operator's real binding keys never reach an anonymous caller. Set it to "admin" to bearer-gate the endpoint (via LUNORA_ADMIN_TOKEN) and include the runtime-authored per-check messages and the full kind:key binding names to aid an operator, but a monitor polling it must then send the token.
  • Tune cacheTtlMs if needed. The last computed report is reused within the window so repeated probes (or an unauthenticated flood) don't re-hit the bindings on every request. The defaults differ per endpoint: /_lunora/health (aggregate) defaults to 5000 ms in the "public" posture and 0 in "admin", while /_lunora/health/ready defaults to 0 in both — a load-balancer readiness poll must see a dependency drop on the very next poll. An explicit cacheTtlMs overrides the default for both endpoints, so damping the aggregate probe also starts caching the readiness gate; set 0 to always run live.
  • The Studio's Deployment health page (under Observability) reads these same endpoints live: a quick operator view of liveness, readiness, and per-binding status.
  • Optionally gate CI on it: lunora verify --health-url https://<your-app> probes /_lunora/health and fails the run on a red status. The flag is opt-in, so a plain lunora verify stays offline-safe.

Limits

  • You've read Limits and know which Cloudflare ceiling you'll hit first: DO SQLite size and per-DO request rate are the usual sharding signals, subrequest counts bound fan-out reads.

Advisors as the final gate

  • Codegen's static advisories are clean; they run on every lunora dev save and on deploy, and catch unindexed foreign keys, filters without indexes, unguarded admin routes, and more before they ship.
  • The Studio's Advisors panels (Security, RLS, Insights) show no unaddressed ERROR-level findings against a staging deployment; the runtime lints (hot shards, index utilization, constraint violations) need observed traffic. See Advisors.

See also