Deploy
Cloudflare Workers
Deploy to Cloudflare Workers with @lumiose/adapter-cloudflare: the app runs as a Worker, the static files as assets.
Tested locally in wrangler dev (workerd), not yet on Cloudflare itself: see the
verification table.
#Set it up
pnpm add -D @lumiose/adapter-cloudflare wrangler// vite.config.ts
import { cloudflare } from "@lumiose/adapter-cloudflare";
import lumiose from "@lumiose/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [lumiose({ adapter: cloudflare() })],
});// wrangler.jsonc, next to package.json
{
"name": "my-app",
"main": "dist/_worker.js",
"compatibility_date": "2026-09-01",
// AsyncLocalStorage, which Lumiose's request APIs use, and process.env for your variables.
"compatibility_flags": ["nodejs_compat"],
"assets": { "directory": "dist/client" },
}Then build and deploy:
pnpm lumiose build
pnpm wrangler deploy # or `wrangler dev` to try the build locally firstlumiose build warns, with a config to copy, when the project has no wrangler.jsonc,
wrangler.json or wrangler.toml. A site whose every route is prerendered builds no Worker: drop
main, and Cloudflare serves the assets alone.
#What the build writes
| File | What it is |
|---|---|
dist/_worker.js |
The Worker: dist/server/entry.js's fetch, called with ctx.platform (below). Wrangler bundles it, with the npm packages your server code imports, when it deploys. |
dist/client |
The static assets. Cloudflare serves a request that matches a file without running the Worker. |
dist/client/_headers |
lumiose({ headers }) and Lumiose's default headers for the static files, plus a year of caching (immutable) for /assets/*, whose files Vite names by content hash, except files you put in public/assets/ (! cache-control). The Worker's responses get the same headers from the handler. |
dist/client/.assetsignore |
Only when prerendered pages have form actions: their HTML is left out of the assets, because Cloudflare answers a POST to a static file itself (405). The Worker renders those pages, fully resolved, and runs their actions. |
Cloudflare compresses responses itself, so the build writes no .br/.gz copies, and the server
build leaves out its own static file layer (no node:fs).
#The platform in your code
Middleware, endpoints and actions get Cloudflare's arguments as ctx.platform:
ctx.platform.env: the Worker's bindings (KV, D1, R2, Durable Objects, variables, secrets).ctx.platform.waitUntil(promise):ctx.waitUntil(), for work after the response.ctx.platform.clientAddress: theCF-Connecting-IPheader.
Type your bindings by augmenting PlatformEnv (with @cloudflare/workers-types or the types
wrangler types generates):
// src/routes/api/notes/route.ts
import type { Endpoint } from "lumiose";
declare module "lumiose" {
interface PlatformEnv {
DB: D1Database;
}
}
export const mode = "server";
export const GET: Endpoint<"/api/notes"> = async (ctx) => {
const { results } = await ctx.platform.env.DB.prepare("select * from notes").all();
return Response.json(results);
};Server components read them through middleware: copy what they need to ctx.locals.
Variables and secrets are also in process.env with nodejs_compat (compatibility dates from
2025-04-01), so env("NAME") and ctx.env.NAME work as on Node.
#Trailing slashes
Cloudflare's assets follow assets.html_handling. Match it to lumiose({ trailingSlash }):
trailingSlash |
html_handling |
/about |
|---|---|---|
"ignore" (default) |
"auto-trailing-slash" (default) |
redirects to /about/ (307) |
"always" |
"force-trailing-slash" |
redirects to /about/ |
"never" |
"drop-trailing-slash" |
serves about.html |
With "ignore", Lumiose serves both forms itself, but Cloudflare redirects /about before the
Worker runs.
#Good to know
- Static files skip the Worker, so middleware does not run for them. Headers they need belong in
lumiose({ headers }). - Streaming works: the shell of a page is sent first,
<Suspense>content follows. - Unknown URLs reach the Worker, which renders your
not-foundpage. Without a Worker (a static site), set"not_found_handling": "404-page"inassetsto serve404.html. - The Worker's npm packages must run on Workers: with
nodejs_compat, many Node.js APIs do, but not everything (native modules, the file system of a server).
#API
import { cloudflare } from "@lumiose/adapter-cloudflare";
function cloudflare(): Adapter; // for lumiose({ adapter })