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

Add realtime rooms

Realtime rooms with room route files — schemas, auth, reconnect, and route identity.

Verdict: works. Define a room route, connect from the client, and Outer passes WebSocket upgrades straight through the app proxy to the fleet. The person-facing guide is Show updates as they happen.

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, a 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. Valibot schemas are required. Auth freezes at connect (ctx.user); re-check bans inside onMessage. Cross-origin sockets stay anonymous unless the origin is trusted.

Reconnect with backoff — required

Sockets die on deploy and on fleet sleep: every deploy moves the fleet, and an idle fleet stops after ~30 minutes. Your 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.

Keep route identity on moves

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.

Was this page helpful?