@lunora/nuxt runs Lunora and Nuxt as a single Cloudflare Worker. Instead
of Lunora owning the Cloudflare worker entry (the two-worker split other
adapters use), 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.
Install
pnpm add @lunora/nuxtnuxt (^4.0.0) is an optional peer dependency; the host app supplies the
Nuxt runtime. h3 is also an optional peer (^1.15.0); see
Deploy notes below.
Setup
Register the module and pick the Cloudflare Nitro preset:
export default defineNuxtConfig({
modules: ["@lunora/nuxt"],
nitro: { preset: "cloudflare_module" },
});Add a worker.ts wrapper at the project root and point wrangler.jsonc's
main at it, so the ShardDO Durable Object class is exported from the
deployed worker. Nitro's cloudflare_module output exports only the SSR
handler, so main must point at the wrapper, not the raw
.output/server/index.mjs, or wrangler deploy fails on the missing DO:
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.
Options
Configurable under the lunora key in nuxt.config.ts.
| Option | Default | Description |
|---|---|---|
appEntry | ~/lunora/server | Module specifier of the Lunora app entry, aliased to the #lunora/app virtual. |
prefix | /_lunora | URL prefix the Lunora realtime plane is mounted at. |
How it works
- The route (
addServerHandlerat<prefix>/**): reconstructs a WebRequestfrom the H3 event, resolves the Cloudflareenv/ExecutionContextoff it (tolerating bothevent.context.cloudflareandevent.req.runtime.cloudflare), 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 (options.appEntry), forwarded into the Nitro server bundle vianuxt.options.alias. ShardDOrides to the deployed worker through your rootworker.tswrapper (wrangler.jsonc'smain), which re-exports Nitro's SSR handler andShardDO; Nitro'scloudflare_moduleoutput exports only the SSR handler.
Server data-loading — @lunora/nuxt/server
Re-exports the framework-neutral SSR contract from @lunora/client/ssr
(createServerClient, preloadQuery, serializePreloaded,
deserializePreloaded, preloadedQueryResult, getServerSession, plus the
ArgsOf / AuthLike / FunctionReference / HeadersSource / Preloaded /
ReturnOf / ServerClientOptions / ServerSession types) for the
reactive-loader handoff. Opens no WebSocket and touches no browser globals, so
it is safe to import from a Nitro server route.
import { createServerClient, preloadQuery } from "@lunora/nuxt/server";
import { api } from "./lunora/_generated/api";
// Per request — never reuse a client across requests (token leakage).
const client = createServerClient({ url: process.env.LUNORA_URL!, token });
const preloaded = await preloadQuery(client, api.posts.list, {});Hand the serializable preloaded value to a client component and seed it with
@lunora/vue's hydratePreloaded. Nuxt is Vue, so the
Vue adapter's composables (useQuery, useMutation, hydratePreloaded) are
the reactive layer here; @lunora/nuxt only owns the server-side composition.
Feature flags
@lunora/nuxt ships no flag composable of its own. Since Nuxt is Vue, 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.
<script setup lang="ts">
import { useFlag } from "@lunora/vue";
// Live over the Lunora WS — holds `false` until the server resolves it.
const newHero = useFlag("homepage-hero", false);
</script>
<template>
<NewHero v-if="newHero" />
<ClassicHero v-else />
</template>Deploy notes
Verify before deploy. Single-worker composition rides on two Nitro behaviours that vary across versions, so verify them on your pinned toolchain. (1)
WebSocket upgrade pass-through: the live feed needs Nitro to return your Lunora app's 101 Switching Protocols response (carrying its Cloudflare
webSocket) untouched; RPC (plain JSON) works regardless, so if live subscriptions never connect while RPC does, Nitro is normalising the upgrade response.
(2) The worker.ts wrapper: wrangler.jsonc's main must point at a root worker.ts that re-exports Nitro's SSR handler and ShardDO (Nitro's
cloudflare_module output exports only the SSR handler); if wrangler deploy fails with "ShardDO class not exported", check that main points at the
wrapper and that it re-exports ShardDO. The module warns when worker.ts is missing but can't verify wrangler's main points at it.
h3 version. The peer range is ^1.15.0 (h3 v1, what Nuxt 4 / Nitro 2 run at runtime). If you pin an explicit h3 dependency yourself, pin it to
^1.15.11; npm's h3@2.0.0 is a deprecated stub with no code, exports, or type declarations (the real v2 line is still prerelease-only). The peer will
re-admit v2 once a stable h3 v2 actually ships.