🍡 mochi

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

On this page

WebSocket routes

Mochi.ws(handlers) registers a WebSocket endpoint backed by Bun’s ServerWebSocket. The handler map carries five callbacks — upgrade, open, message, close, drain — and exposes Bun’s pub/sub primitives (ws.subscribe, ws.publish, ws.unsubscribe) on the socket.

// file: src/index.ts
import { Mochi } from 'mochi-framework';

await Mochi.serve({
  routes: {
    '/ws/chat': Mochi.ws({
      open(ws) {
        ws.subscribe('chat');
      },
      message(ws, message) {
        ws.publish('chat', String(message));
        ws.send(String(message));
      },
      close(ws) {
        ws.unsubscribe('chat');
      },
    }),
  },
});

Only message is required.

upgrade

Runs once per HTTP upgrade request. Return a value to attach to ws.data.user, or return false to reject the connection. The route’s URL params are the second argument.

// file: src/index.ts
import { Mochi } from 'mochi-framework';

await Mochi.serve({
  routes: {
    '/ws/:room': Mochi.ws<{ userId: string; room: string }>({
      upgrade(req, params) {
        const userId = req.headers.get('x-user-id');
        if (!userId) return false; // reject the upgrade
        return { userId, room: params.room };
      },
      message(ws, msg) {
        console.log(ws.data.user.userId, ws.data.user.room, msg);
      },
    }),
  },
});

Request context during the handshake

Since: 0.10.0 (not released yet): Earlier versions threw from getRequestContext() inside upgrade.

getRequestContext() works inside upgrade, so cookies and getClientAddress() are available there. handle middleware does not run for WebSocket upgrades, so locals is empty. Later callbacks receive only the socket, so derive anything header-based here and return it on ws.data.user.

// file: src/index.ts
import { Mochi, getRequestContext } from 'mochi-framework';

await Mochi.serve({
  proxy: { addressHeader: 'x-forwarded-for', xffDepth: 1 },
  routes: {
    '/ws/chat': Mochi.ws<{ address: string | null }>({
      upgrade() {
        return { address: getRequestContext().getClientAddress() };
      },
      message(ws, msg) {
        console.log(ws.data.user.address, msg);
      },
    }),
  },
});

open

Fires once after a successful upgrade. Use it to subscribe the socket to topics or seed per-connection state.

open(ws) {
  ws.subscribe('chat');
}

message

Fires for every inbound frame. The payload is string | Buffer — coerce or decode it before use.

message(ws, message) {
  ws.publish('chat', String(message));
}

close

Fires once when the socket closes, with the close code and reason. Use it to release per-connection state and unsubscribe from topics.

close(ws, code, reason) {
  ws.unsubscribe('chat');
}

drain

Fires when the socket’s send buffer drains after a backpressured ws.send. Resume queued writes here.

ws.data

Each socket carries a typed data object. Mochi reserves the internal fields __mochiRoutePattern, __mochiOpenedAt, and __mochiPath. Your upgrade return value is exposed as ws.data.user.

Mochi.ws<{ userId: string }>({
  upgrade(req) {
    const userId = req.headers.get('x-user-id');
    return userId ? { userId } : false;
  },
  message(ws) {
    console.log(ws.data.user.userId);
  },
});

Pub/sub

Every socket exposes ws.subscribe(topic), ws.publish(topic, data), and ws.unsubscribe(topic). To broadcast from outside a handler, capture the server returned by Mochi.serve() and call server.publish(topic, data).

Socket limits

Since: 0.10.0 (not released yet): The websocket serve option was added in 0.10.0.

Mochi.serve({ websocket }) passes Bun’s socket-level options through to every Mochi.ws() route. Mochi owns open, message, close, and drain; everything else — maxPayloadLength, idleTimeout, backpressureLimit, perMessageDeflate — is yours.

await Mochi.serve({
  routes,
  websocket: { maxPayloadLength: 4 * 1024 },
});

Lifecycle events

Every WebSocket emits ws:open, ws:message, and ws:close on mochiEvents. consoleLogger() prints them. See Events for the payload shape.

See it in action

Live demos showing key concepts from this page