Skip to content
Outer
Esc
↑↓navigate↵open⌘Jpreview
On this page

WebSockets guide

Realtime rooms and sockets with *.ws.ts routes — works on Outer today, with reconnect notes.

Verdict: works. Define a room route, connect from the client, and Outer passes WebSocket upgrades straight through the app proxy to the fleet.

1. Define a room

One route file per room (index/[id]/[...slug] nesting allowed; .dev/.prod env suffixes). Each room instance is backed by a Durable Object — standard binding, declared for you:

// routes/chat/[room].ws.ts
import * as v from "valibot";
import { defineRoom } from "void/ws";

// Validation schemas — install with `bun add valibot` if not present.
const ClientMessage = v.object({ type: v.literal("chat.message"), text: v.string() });
const ServerMessage = v.object({ type: v.literal("chat.message"), text: v.string() });

export default defineRoom({
  messages: { client: ClientMessage, server: ServerMessage },
  async onBeforeConnect(ctx) {
    if (!ctx.user) return new Response("Unauthorized", { status: 401 });
  },
  async onMessage(ctx, event) {
    await ctx.room.broadcast({ type: "chat.message", text: event.text });
  },
});
// Client
import { connect } from "void/ws";

const socket = connect("/chat/:room", { params: { room: "general" } });

Events are JSON with a validated shape — no raw string/binary frames. Auth freezes at connect (ctx.user); re-check bans inside onMessage. Cross-origin sockets stay anonymous unless the origin is trusted.

2. Reconnect — required, not optional

Sockets die on deploy and on fleet sleep: every deploy moves the fleet, and an idle fleet stops after ~30 minutes. The client must reconnect with backoff and re-fetch missed messages from D1. Hibernated Durable Object sockets survive short idles but never a move — code for the disconnect.

3. Deploy

Nothing to declare: the route file is the config. Rebuild and commit; the room serves at the same host as the app. One room per file keeps failure domains small. Moving a route keeps its state only when you carry its identity: run void info first for the name: to add to defineRoom, keep the parameter names, and use key: to preserve the old key when a parameter is renamed — otherwise the moved route selects a new Durable Object and looks empty.

Sources

Was this page helpful?