---
title: 'Serve options'
slug: serve-options
description: 'Reference for every configuration option available on Mochi.serve().'
---
## Serve options
`Mochi.serve(options)` boots the Bun server and registers routes. Pass a single `MochiServeOptions` object — every field below is optional except `routes`.
```ts
// file: src/index.ts
import { Mochi } from 'mochi-framework';
await Mochi.serve({
port: 3333,
routes: {
'/': Mochi.page('./src/Home.svelte'),
},
});
```
Response compression is opt-in via the [`compress()` middleware](/docs/middleware/#compress) — add it to `handle: sequence(...)` to negotiate brotli or gzip per client.
### Asset caching
In production (`development: false`), prebuilt JS/CSS bundles served from `assetPrefix` (default `/_mochi`) get `Cache-Control: public, max-age=31536000, immutable` automatically. Filenames are content-hashed, so any change yields a new URL — there's nothing to invalidate. In development the header is omitted so live-reload edits aren't pinned in the browser cache. Public-dir files (`./public/...`) keep Bun's default static-route headers; their URLs are stable, so don't mark them immutable. To override, mutate `response.headers` in a `handle` middleware.
Do **NOT** call `Mochi.serve()` more than once per process; instead, run a second site as a separate process on a different port. A second call throws `Mochi.serve() has already been called. Only one instance is allowed.`
**Shutdown signals.** `Mochi.serve()` installs `SIGTERM` and `SIGINT` listeners that fire the [`mochi:shutdown`](/docs/extensions/#mochishutdown) hook and call `server.stop()`. A second signal force-exits with code 1. Existing user listeners on those signals are not displaced — Node.js dispatches signals to every registered listener.
### Options reference
- `port`: TCP port to listen on. No default — set it explicitly.
- `hostname`: Interface to bind. Defaults to Bun's default (`0.0.0.0`).
- `development`: Enables live reload, debug bar, and the dev error overlay. Default: `true`.
- `liveReload`: Enable the dev-mode live-reload WebSocket (`/__mochi_live_reload` + the `mochi-live-reload` web component). Default: matches `development`. Set to `false` to keep the debug bar but skip the WS — useful behind a proxy where the socket is flaky.
- `routes`: `Record` of route paths to `Mochi.page` / `Mochi.api` / `Mochi.ws` / `Mochi.sse` registrations.
- `fetch`: `(req, server) => Response` fallback handler invoked when no route matches. Default: built-in 404.
- `manifest`: Path to a prebuilt manifest JSON. Default: `/manifest.json`.
- `htmlShell`: Path to an `.html` template or an inline template string. Default: built-in shell. See `Custom HTML shell`.
- `handle`: A `Handle` (or `sequence(...)` of them) that wraps every request. See `Middleware (hooks)`.
- `errorPage`: Path to a Svelte component rendered for uncaught page errors and unmatched routes. Default: built-in minimal error page. See `Error handling`.
- `handleError`: `HandleError` hook invoked before the error page renders; may override status/message or return a `Response`. See `Error handling`.
- `compressServerIslandProps`: Deflate-compress server-island props when it reduces size. Default: `true`.
- `logger`: Built-in request logger. Default: `{ enabled: true }`. Pass `{ enabled: false }` to disable, or override `slowThreshold` / `verySlowThreshold`.
- `publicDir`: Directory served as static assets (cwd-relative). Default: `./public`.
- `outDir`: Directory for build artifacts and dev cache (cwd-relative). Default: `./.mochi`.
- `assetPrefix`: URL prefix for framework client assets and the server-island endpoint. Must start with `/`, must not be `/`, must not end with `/`, must not contain whitespace or `..`. Default: `/_mochi`.
- `additionalWatchPaths`: Extra dev-mode watcher paths added to the defaults `src` and `public`. Default: `[]`.
- `svelteConfigPath`: Path to a Svelte config file. Default: `./svelte.config.js`. See `Svelte config`.
- `csrf`: `MochiCsrfOptions` controlling the origin-header check. See `CSRF` below.
- `proxy`: `MochiProxyOptions` describing trusted reverse-proxy headers. See `Proxy` below.
- `hooks`: `MochiHooks` map of named lifecycle hooks. See `Extensions (hooks & filters)`.
- `filters`: `MochiFilters` map of named value-replacement filters. See `Extensions (hooks & filters)`.
- `warmup`: Warm the SSR pipeline at startup by invoking every static page route once. `boolean | { enabledInProd: boolean; enabledInDev: boolean }`. `true` warms in **production only**; pass the object form for per-mode control. Default: `false`. See `Route warmup` below.
Do **NOT** set `assetPrefix` only at runtime when running against a prebuilt manifest; instead, also pass it to the `build()` call (or `--asset-prefix`) so the manifest's baked-in URLs match. The manifest value wins at runtime when the two disagree.
### Route warmup
Components are compiled at startup, but the render pipeline — `serverProps`, Svelte SSR, HTML shell assembly — stays cold until a route is first visited, so the first request to each page pays a one-time penalty. Set `warmup: true` to invoke every static page route once, in the background, immediately after the server starts listening:
```ts
await Mochi.serve({
warmup: true, // warms in production only
routes,
});
```
`warmup: true` warms in **production only** — dev restarts are frequent, and the extra render burst on every reload isn't worth it. For per-mode control, pass an object:
```ts
await Mochi.serve({
warmup: { enabledInProd: true, enabledInDev: false },
routes,
});
```
Warmup is fire-and-forget — the server accepts real traffic immediately, and a [`warmup:complete`](/docs/events/) event fires once the batch finishes. Routes are warmed one at a time so each [`request`](/docs/events/#request) event reports its own render duration rather than a cumulative figure. Warmup requests carry `warmup: true` on their `request` event and log under a `WARM` label instead of `GET`, so they're easy to tell apart from real traffic. Each warmed route runs through its real handler (middleware included) as an anonymous `GET`; a route that throws or returns a `5xx` is counted in `warmup:complete`'s `errorCount`, but the SSR module stays warm regardless.
Routes that aren't fully static — parameter segments (`/docs/:slug`) and `*` catch-alls — are **skipped**, since they have no single canonical URL to warm.
Detect warmup hits from your own code to skip side effects that shouldn't fire for synthetic traffic. Both `event.isWarmup` (in [middleware](/docs/middleware/)) and [`getRequestContext().isWarmup`](/docs/request-context/) (in `serverProps`, components, API handlers) are `true` during warmup:
```ts
const analytics: Handle = async ({ event, resolve }) => {
if (!event.isWarmup) track(event.url.pathname); // skip warmup hits
return resolve(event);
};
```
Warmup requests run the full handler, so any side effects in `serverProps` (logging, cache priming, counters) fire once at startup unless you guard them with `getRequestContext().isWarmup`. The request carries no cookies or session, so auth-gated `serverProps` will see an anonymous visitor.
### CSRF
`csrf` gates state-mutating form submissions (`POST` / `PUT` / `PATCH` / `DELETE` with `application/x-www-form-urlencoded`, `multipart/form-data`, or `text/plain`) against an origin-header check; the request's `Origin` must match the expected origin or appear in `csrf.trustedOrigins`. JSON endpoints rely on the browser's CORS preflight and aren't checked.
- `checkOrigin`: Compare `Origin` against the resolved expected origin. Default: `true`.
- `trustedOrigins`: Extra origins to allow even when they don't match. Default: `[]`.
```ts
await Mochi.serve({
proxy: {
origin: 'https://app.example.com',
},
csrf: {
trustedOrigins: ['https://embed.partner.com'],
// checkOrigin: false, // disable the check entirely
},
routes,
});
```
In production the check refuses every form mutation until `proxy.origin` (or `proxy.hostHeader`) is set, so the deployment break is loud rather than silent. In development the same request is allowed through with a `[mochi]` warning so local work isn't blocked.
Do **NOT** disable `checkOrigin` to silence a 403 in production; instead, configure `proxy.origin` so the framework knows what origin to trust.
### Proxy
`proxy` tells the framework how to recover the public origin (used by the CSRF check) and the real client IP (returned by `getClientAddress()` on the request context) from forwarded headers. Behind a load balancer, CDN, or tunnel, the connection Bun sees is the proxy, not the client.
- `origin`: Explicit public origin (e.g. `'https://my.site'`). Wins over the header options.
- `protocolHeader`: Forwarded-protocol header (typically `'x-forwarded-proto'`).
- `hostHeader`: Forwarded-host header (typically `'x-forwarded-host'`).
- `portHeader`: Forwarded-port header (typically `'x-forwarded-port'`). Only needed when the public port differs.
- `addressHeader`: Forwarded client-IP header (e.g. `'true-client-ip'`, `'x-forwarded-for'`).
- `xffDepth`: Number of trusted proxies in front of the server when `addressHeader` is `'x-forwarded-for'`. Default: `1`.
- `requestIdHeader`: Forwarded correlation-id header (typically `'x-request-id'`). Seeds `getRequestContext().requestId` when set on the inbound request.
```ts
await Mochi.serve({
proxy: {
// Either pin the public origin…
origin: 'https://my.site',
// …or derive it from forwarded headers.
protocolHeader: 'x-forwarded-proto',
hostHeader: 'x-forwarded-host',
portHeader: 'x-forwarded-port',
// Client IP for getClientAddress():
addressHeader: 'x-forwarded-for',
xffDepth: 3, // 3 trusted proxies in front of the server
},
routes,
});
```
Do **NOT** set the header options when the proxy is not trusted to overwrite them; instead, leave them unset. Clients can spoof these headers when reaching the app directly.
#### `xffDepth` and spoofing
`X-Forwarded-For` is comma-separated — each proxy appends the address it saw. With three trusted proxies and no spoofing:
```
client, proxy1, proxy2
```
The framework reads from the **right**, skipping `xffDepth - 1` trusted proxies, so `xffDepth: 3` returns `client`. Reading from the right blocks spoofing: a client setting its own `X-Forwarded-For` gets pushed leftward by each trusted proxy.
```
spoofed, client, proxy1, proxy2 # xffDepth: 3 → "client" (spoofed entry ignored)
```
Do **NOT** read `request.headers.get('x-forwarded-for')` directly when you want a trusted client IP; instead, use `getClientAddress()` with the right `xffDepth`. Read the raw header only when you want the leftmost address (e.g. geolocation where realness matters more than trust).
#### `getClientAddress()`
```ts
import { getRequestContext, Mochi } from 'mochi-framework';
export const handler = Mochi.api(() => {
const ip = getRequestContext().getClientAddress();
return Response.json({ ip });
});
```
Without `proxy.addressHeader`, this returns Bun's connecting `remoteAddress` (or `null` if unavailable).