SSR framework for Svelte 5 + Bun with islands-based selective hydration
Serve options
Selected Mochi.serve() options (see MochiServeOptions for the full list):
| Option | Default | Description |
|---|---|---|
port | — | Port to listen on |
development | true | Enables live reload, debug bar, and error overlay |
htmlShell | built-in | Path or template string for the HTML shell |
publicDir | ./public | Directory served as static assets |
outDir | ./.mochi | Directory for build artifacts and dev cache |
assetPrefix | /_mochi | URL prefix for framework client assets and the server-island endpoint |
additionalWatchPaths | [] | Extra dev-mode file watcher paths, added to the defaults src and public |
gzip | true | Gzip-compress responses when the client sends Accept-Encoding: gzip |
compressServerIslandProps | true | Deflate-compress server island props when it reduces size |
logger | { enabled: true } | Built-in request logger; pass { enabled: false } to disable |
await Mochi.serve({
port: 3333,
gzip: false, // disable response compression (e.g. when a reverse proxy handles it)
routes,
});CSRF
State-mutating form submissions (POST / PUT / PATCH / DELETE with application/x-www-form-urlencoded, multipart/form-data, or text/plain) are gated by an origin-header check: the request’s Origin must match the expected origin (see Proxy below) or appear in csrf.trustedOrigins. JSON endpoints rely on the browser’s CORS preflight and aren’t checked.
Safe by default. In production the check refuses every form mutation until you set proxy.origin (or proxy.hostHeader) so the framework knows what origin to trust. The 403 body explains the missing config so the deployment break is loud rather than silent. In development the same request is allowed through with a [mochi] warning instead, so local work isn’t blocked. Opt out entirely with csrf.checkOrigin: false if you really mean to.
A configured production setup looks like this:
await Mochi.serve({
proxy: {
origin: 'https://app.example.com',
},
csrf: {
trustedOrigins: ['https://embed.partner.com'], // optional extra origins
// checkOrigin: false, // disable the check entirely
},
routes,
});Proxy
Behind a reverse proxy (load balancer, CDN, ngrok), the connection Bun sees is from the proxy, not the client. proxy options tell 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.
Only set the header options when the proxy is trusted to overwrite them — clients can spoof these headers when reaching the app directly.
| Option | Use when | Example |
|---|---|---|
origin | The public URL is fixed and known. Wins over the header options. | 'https://my.site' |
protocolHeader | The proxy sets a forwarded-protocol header you trust. | 'x-forwarded-proto' |
hostHeader | The proxy sets a forwarded-host header you trust. | 'x-forwarded-host' |
portHeader | The proxy listens on a non-standard port and forwards it. | 'x-forwarded-port' |
addressHeader | The proxy forwards the client IP (e.g. True-Client-IP, X-Forwarded-For). | 'x-forwarded-for' |
xffDepth | Number of trusted proxies in front of the server, when addressHeader is x-forwarded-for. | 3 |
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', // optional, only if the public port differs
// Client IP for getClientAddress():
addressHeader: 'x-forwarded-for',
xffDepth: 3, // 3 trusted proxies in front of the server
},
routes,
});xffDepth and spoofing
X-Forwarded-For is a comma-separated chain — each proxy appends the address it saw. With three trusted proxies in front of the server and no spoofing:
client, proxy1, proxy2The 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)If you need the leftmost address instead — for example, geolocation where the IP being real matters more than its being trusted — read request.headers.get('x-forwarded-for') directly in your handler.
getClientAddress() on the request context
import { getRequestContext } 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).