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.jsoncdeclares the Durable Object bindings (SHARD, andSCHEDULERif you schedule work), the D1 database for.global()tables, and the R2 bucket for file storage; see the reference config in Deployment. - The
migrationsblock lists your DO classes undernew_sqlite_classes(Durable Object class migrations are wrangler config, not SQL). -
lunora verifypasses. It validateswrangler.jsonc, dry-runs codegen, and runstsc --noEmitwithout writing files; a stale_generated/or a mistyped binding fails here instead of at runtime. -
lunora doctoris clean: it preflights the project's wrangler bindings, placeholder values, and dev secrets.lunora doctor --fixcreates the D1 databases the config names (reusing any that already exist) and writes their ids in, and mints the local.dev.varssecrets. It refuses to write.dev.varsunless git ignores it.--fix --dry-runprints 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_date2026-10-01(flagdurable_object_io_tasks_prevent_eviction), a pending timer,waitUntilor 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. ALunoraClientor anEventsSyncpoll 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 withdurable_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) untilclose()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.examplehas a production value:wrangler secret put NAME --env production, or push in bulk withlunora env push --prod. -
LUNORA_RELAY_SECRETis 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 generatemints 32-byte-hex values for everything locally generatable; provider keys (Resend, Stripe, …) come from the provider dashboard. -
lunora env doctorexits zero; it checks.dev.varsagainst 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 deployaborts 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_TOKENis set as a production secret and is at least 24 characters (theadmin-token-weaksecurity advisor flags shorter ones; see Security). - It is stored only in your secret manager/CI, never in
wrangler.jsoncvarsor 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
Authorizationheader, 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 viaPOST /_lunora/admin/ws-token(itself gated by the master token in theAuthorizationheader). 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 theAuthorizationheader, 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/ratelimitmiddleware; thepublic_mutation_without_ratelimitadvisor lint finds the gaps. - Consider
protectPublic({ rateLimit, captcha })from@lunora/serverto 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) stampsctx.authon every call; see Authentication. -
authAdminis set if you use the Studio's auth panel (the deprecatedauthIntrospectoroption 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+ additiveADD 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 withlunora migrate up) have been dry-run against a copy where the change is destructive. Runs are admin-gated: setLUNORA_ADMIN_TOKENor pass--token. - DO class changes are reflected in the
migrationsblock ofwrangler.jsonc.
Observability
- An observability sink is configured on
createWorker:otlpSinkto a collector,webhookSinkto an error tracker (Sentry, PostHog, …),analyticsEngineSink, or several viacombineSinks. 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
traceparentitself and you want end-to-end waterfalls,trustInboundTraceContextis 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 logstails a deployed worker live; durable retention needs atail_consumersWorker 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/healthis the aggregate probe. It runs every registered check and returns200when all critical dependencies (the Durable Object namespace, D1) are up,503when any critical dependency is down. A non-critical failure (a presence gap on R2, a queue) degrades the reportedstatusto"degraded"but keeps the200, since the deployment still serves traffic. The body'sstatusis one of"healthy","degraded", or"unhealthy".GET /_lunora/health/readyis the readiness gate. It runs only the readiness checks and returns503whenever 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/readyfor 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#2suffix if a kind repeats) plusup/down, so the operator's real binding keys never reach an anonymous caller. Set it to"admin"to bearer-gate the endpoint (viaLUNORA_ADMIN_TOKEN) and include the runtime-authored per-checkmessages and the fullkind:keybinding names to aid an operator, but a monitor polling it must then send the token. - Tune
cacheTtlMsif 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 to5000ms in the"public"posture and0in"admin", while/_lunora/health/readydefaults to0in both — a load-balancer readiness poll must see a dependency drop on the very next poll. An explicitcacheTtlMsoverrides the default for both endpoints, so damping the aggregate probe also starts caching the readiness gate; set0to 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/healthand fails the run on a red status. The flag is opt-in, so a plainlunora verifystays 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 devsave 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
- Deployment: the deploy flow this page gates.
- Security: the secure-by-default posture in detail.
- Versioning & stability: what you're pinning when you pin a version.