Deploy your framework
One worker, one deploy — how a composed Lunora + meta-framework app ships to Cloudflare.
Last updated:
A Lunora + meta-framework app is one Cloudflare Worker. Your framework's pages, API routes, and SSR loaders share the same Worker that serves Lunora realtime. There is no second service to provision and no second deploy to coordinate. This page covers how the single-worker bundle is assembled per framework class and how it ships.
For the framework-neutral deploy mechanics (wrangler.jsonc bindings, secrets,
lunora deploy, log tailing) see the Deployment reference.
This page is the framework-composition layer on top of it.
One worker, two dispatch flows
However your app is composed, the resulting Worker dispatches by path:
/api/auth/*→@lunora/auth- explicit routes → webhooks / callbacks
- everything else → your framework's SSR handler
/_lunora/rpc→ query / mutation RPC/_lunora/ws→ subscriptions / deltas/_lunora/admin/*→ studio / observability
Lunora realtime lives under the reserved /_lunora/* namespace, so it never
collides with your framework's routes, and a 500 from SSR does not take down the
realtime endpoints. See Bring your framework
for the request-resolution detail.
Class A — Lunora owns the worker entry
TanStack Start, React Router (Vite), SolidStart. These are Vite-native, so
Lunora owns the Worker entry and drops the framework's SSR handler straight into
createWorker({ httpRouter }). One Vite build, one bundle, one main.
// src/server.ts — the wrangler `main`
import { createWorker } from "lunorash/runtime";
import { ShardDO } from "lunorash/do";
import { ssrHandler } from "./framework-entry"; // your framework's SSR fetch handler
export default createWorker({
httpRouter: ssrHandler, // pages / API / SSR loaders
shardDO: ShardDO,
// auth, scheduler, … as needed
});
export { ShardDO };// wrangler.jsonc
{
"name": "my-app",
"main": "src/server.ts",
"compatibility_date": "2026-04-07",
"compatibility_flags": ["nodejs_compat", "web_socket_auto_reply_to_close"],
"durable_objects": { "bindings": [{ "name": "SHARD", "class_name": "ShardDO" }] },
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ShardDO"] }],
}Deploy is the standard flow:
pnpm lunora deploy # codegen → D1 migrations → wrangler deployBecause the SSR handler and Lunora run in the same Worker, an SSR loader can
call Lunora over a same-process loopback (/_lunora/rpc) with near-zero latency.
An in-process serverQuery fast-path that skips even the loopback is in progress.
See Reactive loaders.
Class B — the framework owns the CF adapter
SvelteKit, Nuxt, Astro. These ship their own Cloudflare adapter and build
their own Worker, so Lunora does not take over the entry. Instead the Lunora
worker composition is injected into the framework's server entry / hooks: Lunora
realtime mounts under /_lunora/*, and the framework keeps everything else.
- SvelteKit —
@sveltejs/adapter-cloudflarebuilds the Worker; the Lunora realtime handler is composed in via the serverhandlehook (the@lunora/svelteadapter ships awithLunora()-style wrapper).ShardDOand the migrations are declared in the adapter'swrangler.jsonc. - Nuxt — Nitro's
cloudflare-modulepreset builds the Worker; Lunora is composed into the Nitro server entry, withShardDOexported from the same module. - Astro —
@astrojs/cloudflarebuilds the Worker; Lunora mounts under/_lunora/*from the Astro middleware / server entry.
The deploy command is still the framework's own build plus wrangler deploy. The
one thing you must reconcile by hand for class B today is that the Durable
Object class and its migration (ShardDO, new_sqlite_classes) are declared in
the framework adapter's wrangler.jsonc, because the framework owns that file,
not Lunora.
Status: be honest about the tier. Class-A deploy (TanStack Start) is proven. Class-B composition (SvelteKit / Nuxt / Astro) is preview: the templates and adapters exist, but the hook-injection and single-bundle deploy have not been run end-to-end in production. Treat the class-B steps above as the intended shape, and verify against your framework's current Cloudflare adapter when you scaffold.
Class C — SPA / SSR-less
A standalone SPA (the vite / standalone templates) deploys as a Lunora Worker
with no SSR loaders; the client adapter opens the live subscription on the
client. This is the original Lunora deploy and is fully shipped. See
Deployment.
Wrangler reconciliation
Lunora's Vite plugin reconciles the bindings it needs (SHARD, SESSION,
SCHEDULER, DB) into wrangler.jsonc and validates the result. For class-A
apps the Lunora plugin owns the config; for class-B apps it runs in
cloudflare: false mode alongside the framework's plugin and validates the
framework-owned config rather than taking it over. Automatic framework detection
and full one-worker emit for class-A frameworks are in progress.
See also
- Deployment — bindings, secrets,
lunora deploy, log tailing - Bring your framework — the composition matrix
- Manual end-to-end verification — prove live loaders work
- @lunora/runtime —
createWorker, thehttpRouterseam