Reference
Server build (dist/server/entry.js)
What lumiose build writes for servers: one web-standard fetch handler for pages, endpoints, actions and static files.
Concepts: Rendering modes (the build), Server
(the request context). For Node, use @lumiose/adapter-node.
#default export
declare const handler: {
fetch(request: Request, platform?: Partial<Platform>): Promise<Response>;
};
export default handler;A request goes through, in order:
- Static files (
GETandHEADbelow Vite'sbase): the files ofdist/client, with the same rules as@lumiose/adapter-node: prerendered pages by URL (/aboutservesabout/index.html, orabout.htmlwithtrailingSlash: "never", and the"always"/"never"policies redirect with308), the.br/.gzcopies the build wrote for clients that accept them (Vary: Accept-Encoding), a year of caching (immutable) for content-hashed assets and revalidation (ETag,Last-Modified,304) for everything else. Dot files (other than.well-known),..and_headersare never served. The directory is read withnode:fsand indexed on the first request: files added later are not served until a restart. - The app: endpoints, form actions and pages, rendered shell first and streamed
(
<Suspense>). The request'ssignalaborts the render when the client goes away. Bodies are bounded bylumiose({ limits })(413). - Headers:
lumiose({ headers })for the path, and Lumiose's defaults (X-Content-Type-Options,Referrer-Policy), go on every response that does not set them itself.
It does not compress the app's responses on the fly (static files use their precompressed
copies): put a CDN or proxy that compresses in front. On Node, @lumiose/adapter-node compresses.
The function works detached (const { fetch } = handler). Builds made with a deploy adapter
(Cloudflare, Vercel, Netlify) leave the static files to the platform, which serves dist/client
before calling the function.
#platform
interface Platform {
/** The platform's bindings (Cloudflare's env); {} where there are none. */
readonly env: PlatformEnv;
/** Keeps the host running `promise` after the response; elsewhere it just runs. */
waitUntil(promise: Promise<unknown>): void;
/** The client's IP address as the host sees it (a proxy's, behind one). */
readonly clientAddress: string | undefined;
}
interface PlatformEnv {} // augment: declare module "lumiose" { interface PlatformEnv { … } }The second argument becomes ctx.platform in middleware, endpoints and actions
(Server). Every field has a fallback: env is {}, waitUntil lets the
promise run and reports a rejection like any server error, clientAddress is undefined.
Server environment variables stay in ctx.env and env(name).
What each host passes:
| Host | env |
waitUntil |
clientAddress |
|---|---|---|---|
@lumiose/adapter-node, lumiose dev |
{} |
fallback | the socket's remote address |
Bun (Bun.serve passes its server) |
{} |
fallback | server.requestIP(request) |
Deno (deno serve passes its info) |
{} |
fallback | info.remoteAddr.hostname |
workerd (fetch(request, env, ctx)) |
the bindings | ctx.waitUntil |
CF-Connecting-IP |
| Your own server | what you pass | what you pass | what you pass |
Behind a reverse proxy, clientAddress is the proxy's: read the header your proxy sets
(X-Forwarded-For, X-Real-IP) in middleware if you trust it.
Type your platform's bindings by augmenting PlatformEnv:
// src/env.d.ts
declare module "lumiose" {
interface PlatformEnv {
DB: D1Database; // Cloudflare D1, typed by @cloudflare/workers-types
}
}
export {};#createHandler
export function createHandler(options?: {
/** ctx.env and env(). Default: process.env. */
env?: Readonly<Record<string, string | undefined>>;
/** Unexpected errors (not redirect(), notFound() or error(status)). Default: console.error. */
onError?: (error: unknown, request: Request) => void;
}): { fetch(request: Request, platform?: Partial<Platform>): Promise<Response> };The app alone: no static files and no lumiose({ headers }). @lumiose/adapter-node uses it
behind its own static file server.
onError is the host's log. With a src/instrumentation.server.ts,
the build bundles it into the server entry: every handler (the default export, createHandler(),
the adapters') runs its register() before the first request, once per server instance, and its
onRequestError() receives the same errors with the route and the phase, after onError.
Prerendering at build time does not load it.
#config
export const config: {
base: string; // Vite's base, with slashes: "/" or "/docs/"
trailingSlash: "ignore" | "always" | "never";
assetsDir: string; // "assets": content-hashed files
headers?: HeadersOption; // lumiose({ headers })
};What servers of dist/client need to know about the build.
#Run it
Build first (lumiose build), then start the entry from the project directory, so that the npm
dependencies it imports resolve from node_modules.
Bun starts a server for a module whose default export has fetch
(PORT, default 3000):
NODE_ENV=production PORT=3000 bun dist/server/entry.jsDeno serves it with deno serve (--port, default 8000). It reads dist/client and the
environment:
deno serve --allow-net --allow-read --allow-env --port 3000 dist/server/entry.jsNode: use @lumiose/adapter-node (createServer()), which adds on-the-fly
compression and Node's streaming. Any server that takes a fetch handler works too:
// server.ts, with Hono's Node server: `node server.ts`
import { serve } from "@hono/node-server";
import handler from "./dist/server/entry.js";
serve({ fetch: (request) => handler.fetch(request), port: 3000 });For Cloudflare, Vercel, Netlify, static hosts and the rest, see the deploy guides.
#Example
Using the platform in an endpoint: record a visit after the response, without delaying it.
import type { Endpoint } from "lumiose";
export const POST: Endpoint = (ctx) => {
const visitor = ctx.platform.clientAddress ?? "unknown";
ctx.platform.waitUntil(
fetch("https://analytics.example.com/hit", { method: "POST", body: visitor }),
);
return new Response(null, { status: 204 });
};