Nuxt
Run Lunora and Nuxt as one Cloudflare Worker — the @lunora/nuxt module mounts /_lunora/** into Nitro, plus reactive-loader server helpers.
Last updated:
@lunora/nuxt is a Nuxt module that runs Lunora and Nuxt as a single
Cloudflare Worker. Instead of Lunora owning the worker entry, it is mounted
inside Nitro: the module registers a server route at /_lunora/** that
forwards every Lunora RPC, WebSocket upgrade, and admin request to your Lunora
app in-process. One wrangler.jsonc, one deploy, a same-origin client.
Nuxt is Vue, so the reactive layer is @lunora/vue
(useQuery, useMutation, hydratePreloaded) — @lunora/nuxt owns only the
server-side composition. This is the Nuxt-specific companion to
Bring your framework (Nuxt is a
class-B framework there) and
Reactive loaders; for the full reference
see @lunora/nuxt.
Preview. Class-B single-worker composition is preview maturity — see the verify before deploy checks below before shipping.
Install
pnpm add @lunora/nuxt @lunora/vuenpm install @lunora/nuxt @lunora/vueyarn add @lunora/nuxt @lunora/vuebun add @lunora/nuxt @lunora/vueSetup
Register the module and target Nitro's Cloudflare module preset:
export default defineNuxtConfig({
modules: ["@lunora/nuxt"],
nitro: { preset: "cloudflare_module" },
});Nitro's cloudflare_module output exports only the SSR handler, so the
deployed worker needs a wrapper that also exports the ShardDO Durable Object
class. Add a worker.ts at the project root and point wrangler.jsonc's
main at it — not at the raw .output/server/index.mjs, or wrangler deploy
fails on the missing DO class:
export { default } from "./.output/server/index.mjs";
export { ShardDO } from "./lunora/server";{ "main": "worker.ts" }lunora/server.ts is your built Lunora app (defineApp().build()) — its
default export is the worker (a fetch entrypoint), and it re-exports
ShardDO. The module aliases the #lunora/app virtual to it and serves it at
the /_lunora/** route.
Options
| Option | Default | Description |
|---|---|---|
appEntry | ~/lunora/server | Module specifier of the Lunora app entry (#lunora/app). |
prefix | /_lunora | URL prefix the Lunora realtime plane is mounted at. |
export default defineNuxtConfig({
modules: ["@lunora/nuxt"],
lunora: { appEntry: "~/server/lunora-app", prefix: "/_lunora" },
});How it works
- The route (
addServerHandleratprefix/**) reconstructs a WebRequestfrom the H3 event, resolves the Cloudflareenv/ExecutionContextoff it, and forwards to your app'sfetch. A missing Cloudflare runtime answers a clear 500. - The
#lunora/appalias points the route's worker import at your app entry, forwarded into the Nitro server bundle. ShardDOrides to the deployed worker through your rootworker.tswrapper (wrangler.jsonc'smain), which re-exports Nitro's SSR handler andShardDO.
Because everything is same-origin, the browser client needs no URL — the
default page-origin client from @lunora/vue loops
/_lunora/ws straight back into the composed worker.
Reactive loaders
@lunora/nuxt/server re-exports the framework-neutral SSR helpers
(createServerClient, preloadQuery, …) from @lunora/client/ssr — safe to
import from a Nitro server route (no WebSocket, no browser globals). Preload
on the server, hand the serializable token to the page, and seed a live
ref with @lunora/vue's hydratePreloaded:
import { createServerClient, preloadQuery } from "@lunora/nuxt/server";
import { api } from "../../lunora/_generated/api";
export default defineEventHandler(async () => {
// Per request — never reuse a client across requests (token leakage).
const client = createServerClient({ url: process.env.LUNORA_URL!, token });
return { preloaded: await preloadQuery(client, api.posts.list, {}) };
});<script setup lang="ts">
import { hydratePreloaded } from "@lunora/vue";
const { data } = await useFetch("/api/posts");
// Seeded from SSR on first read, then live.
const posts = hydratePreloaded(data.value!.preloaded);
</script>See Reactive loaders for the full handoff.
Feature flags
@lunora/nuxt ships no flag composable of its own — read ctx.flags
server-side (a Nitro route, a function, or a reactive loader) and pass the
resolved value down, or call useFlag / useFlags from @lunora/vue
directly in a component for live updates over the WebSocket. Requires
@lunora/flags wired in lunora/flags.ts.
Verify before deploy
Single-worker composition rides on two Nitro behaviours that vary across versions — verify them on your pinned toolchain:
- WebSocket upgrade pass-through. The live feed needs Nitro to return
your Lunora app's
101 Switching Protocolsresponse (carrying its CloudflarewebSocket) untouched. RPC (plain JSON) works regardless; if live subscriptions never connect while RPC does, Nitro is normalising the upgrade response. - The
worker.tswrapper.wrangler.jsonc'smainmust point at a rootworker.tsthat re-exports Nitro's SSR handler andShardDO. Ifwrangler deployfails with "ShardDO class not exported", check thatmainpoints at the wrapper. The module warns whenworker.tsis missing but can't verify wrangler'smainpoints at it.
See also
- @lunora/nuxt — the full module reference (incl. the h3 version note)
- Vue — the reactive layer:
useQuery/useMutation/hydratePreloaded - Bring your framework — composition + adapter maturity
- Reactive loaders — the SSR-seed → live handoff