| name | lunora-deploy |
| description | Deploys a Lunora app to Cloudflare. Use for `lunora deploy`, wrangler.jsonc bindings (SHARD/SESSION DOs, D1, R2), provisioning databases/buckets, secrets (`wrangler secret` vs `.dev.vars`), the `lunora doctor` preflight, the schema-drift gate, and dev-vs-prod separation. |
Lunora Deploy
Ship a Lunora app to Cloudflare Workers + Durable Objects. Unlike a managed
backend, deployment owns real Cloudflare resources — Durable Object bindings, a
D1 database, R2 buckets, and secrets — so the work is mostly making
wrangler.jsonc and the remote resources line up.
When to Use
- Deploying to production (or a Cloudflare environment) for the first time.
- A deploy fails on a binding, a placeholder id, or the schema-drift gate.
- Provisioning D1 / R2 / secrets for a Lunora app.
When Not to Use
- Local development — that is
lunora dev (see lunora-quickstart).
- A schema/data change that needs migrating — do that first with
lunora-migration-helper, then deploy.
What lunora deploy Does
lunora deploy runs a fixed pipeline:
- Codegen — regenerates
lunora/_generated/ and typechecks.
- Validate
wrangler.jsonc — required compatibility_date, the
nodejs_compat flag, and the SHARD Durable Object binding.
- Schema-drift gate — blocks if the committed baseline
(
lunora/.lunora-schema.json) drifted with a breaking change and no
accompanying migration. The baseline is re-blessed only after the deploy
succeeds.
wrangler deploy — builds and pushes the worker (and any container
images).
Useful flags: --env <name> (Cloudflare environment), --migrate (run pending
data migrations against the live worker after deploy, with --migrate-token /
--migrate-url), --allow-schema-drift (override the gate — use sparingly), and
--update-schema-baseline (re-bless the baseline with the current shape).
Preflight: lunora doctor
Run the read-only preflight before deploying. It checks:
wrangler.jsonc present with the SHARD durable-object binding.
- D1
database_ids are real, not placeholders (<replace> / empty).
send_email destination addresses aren't placeholders.
.dev.vars secret-looking keys are filled.
- Declared containers are exported by the worker entry.
lunora doctor
lunora verify and lunora prepare run related checks (drift gate, wrangler
validation) — wire lunora verify into CI to catch drift before a deploy.
wrangler.jsonc — the binding contract
A Lunora worker needs the ShardDO (and SessionDO when auth is wired), the
SQLite-DO migration tag, and whatever D1/R2 the app uses:
{
"name": "my-app",
"main": "src/index.ts",
"compatibility_date": "2026-04-07",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{ "name": "SHARD", "class_name": "ShardDO" },
{ "name": "SESSION", "class_name": "SessionDO" },
],
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ShardDO", "SessionDO"] }],
"d1_databases": [{ "binding": "DB", "database_name": "my-app-global", "database_id": "<replace>" }],
"r2_buckets": [{ "binding": "FILES", "bucket_name": "my-app-files" }],
}
lunora dev auto-reconciles most of this (and lunora registry add adds the
bindings an item needs), but the resources themselves must exist and their
ids must be filled in:
wrangler d1 create my-app-global
wrangler r2 bucket create my-app-files
The DO class_names must be exported by your worker entry (the
createShardDO() / generated container exports) — wrangler rejects a binding
whose class the worker doesn't export. lunora doctor surfaces a missing
container export proactively.
Secrets: wrangler secret, not .dev.vars
.dev.vars is dev only — it is git-ignored and never deployed. Production
secrets (BETTER_AUTH_SECRET, RESEND_API_KEY, provider client secrets, …) go
into Cloudflare:
wrangler secret put BETTER_AUTH_SECRET
wrangler secret list
Set every secret your app reads (mirror the secret-looking keys in .dev.vars)
before the first request hits production.
Dev vs Production
- Development:
lunora dev (Vite + workerd + Studio + codegen-on-save). The
schema baseline and .dev.vars belong to dev.
- Production:
lunora deploy. Separate D1 database / R2 buckets / secrets
from dev. Never point a dev worker at prod resources.
For .global() table DDL, generate and commit SQL migrations with lunora migrate generate before deploying; @lunora/d1's runner applies them. For data
backfills, deploy first, then lunora deploy --migrate (or lunora migrate up --prod). See lunora-migration-helper.
Common Pitfalls
- Placeholder
database_id. The D1 binding ships with <replace>; run
wrangler d1 create and paste the id. lunora doctor catches this.
- DO class not exported.
wrangler deploy fails if a class_name isn't
exported by the worker entry — export ShardDO/SessionDO/generated
containers.
- Secrets only in
.dev.vars. They never reach production; use wrangler secret put for every prod secret.
- Bypassing the drift gate.
--allow-schema-drift ships a breaking schema
with no migration — stage the change (lunora-migration-helper) instead.
- Deploying with uncommitted codegen. Commit
lunora/_generated/ and
lunora/.lunora-schema.json so CI and the gate see the same baseline.
Checklist