🍡 mochi

SSR framework for Svelte 5 + Bun with islands-based selective hydration

Serve options

Selected Mochi.serve() options (see MochiServeOptions for the full list):

OptionDefaultDescription
portPort to listen on
developmenttrueEnables live reload, debug bar, and error overlay
htmlShellbuilt-inPath or template string for the HTML shell
publicDir./publicDirectory served as static assets
outDir./.mochiDirectory for build artifacts and dev cache
assetPrefix/_mochiURL prefix for framework client assets and the server-island endpoint
additionalWatchPaths[]Extra dev-mode file watcher paths, added to the defaults src and public
gziptrueGzip-compress responses when the client sends Accept-Encoding: gzip
compressServerIslandPropstrueDeflate-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.

OptionUse whenExample
originThe public URL is fixed and known. Wins over the header options.'https://my.site'
protocolHeaderThe proxy sets a forwarded-protocol header you trust.'x-forwarded-proto'
hostHeaderThe proxy sets a forwarded-host header you trust.'x-forwarded-host'
portHeaderThe proxy listens on a non-standard port and forwards it.'x-forwarded-port'
addressHeaderThe proxy forwards the client IP (e.g. True-Client-IP, X-Forwarded-For).'x-forwarded-for'
xffDepthNumber 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, 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)

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).