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.
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. - 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. Only a legacy client that still puts the master token in the URL needs the opt-out (requireEphemeralWsToken: false, orLUNORA_REQUIRE_EPHEMERAL_WS_TOKENset to0/false/off/no/disabled). Verify it is not set in your deployment.
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.lunora deployruns the D1 migration runner beforewrangler 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,sentrySink,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. It defaults to5000ms in the"public"posture and0(no cache) in"admin"; 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.