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.

One worker, one deploy. Your loaders and your live subscriptions run in the same process, behind the same domain.

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:

  1. /api/auth/*@lunora/auth
  2. explicit routes → webhooks / callbacks
  3. everything else → your framework's SSR handler
  4. /_lunora/rpc → query / mutation RPC
  5. /_lunora/ws → subscriptions / deltas
  6. /_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 deploy

Because 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-cloudflare builds the Worker; the Lunora realtime handler is composed in via the server handle hook (the @lunora/svelte adapter ships a withLunora()-style wrapper). ShardDO and the migrations are declared in the adapter's wrangler.jsonc.
  • Nuxt — Nitro's cloudflare-module preset builds the Worker; Lunora is composed into the Nitro server entry, with ShardDO exported from the same module.
  • Astro@astrojs/cloudflare builds 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