Documentation for Redweb 0.13.3. Install that exact version when following these examples.
Share typed WebSocket contracts
Build a /match service with independent join, move and resume handlers. A shared schema validates payloads and supplies TypeScript types to both sides. Use this when you need a raw socket protocol, such as a game client or a custom realtime client, rather than a server-rendered page.
Explain it like I'm five
The URL is the room's address. A message's type tells the receptionist which person should handle it. The shared contract is the form that says what information that person needs. Checking the form before handing it over prevents a movement handler from receiving a name where a coordinate should be.
Follow the design
- The contract declares
join,move,resumeandstateonce. It is safe to import into a browser bundle because it does not import the server application. - The route binds
/match, enables the contract protocol and registersJoin,MoveandResume. There is no socket decorator layer and no innermessage.actionswitch. - The handlers below receive parsed payloads.
Joincreates an in-memory player session,Movechanges its server-owned coordinates, andResumereclaims it using a private bearer token. - Each sends a validated
stateresponse. A client usesmatch.client(socket)to send and parse typed messages. That wrapper does not open or reconnect its transport; the application creates the WebSocket first.
Connect to ws://localhost:8181/match?redwebVersion=1 during local development. Follow the complete recipe's client example for opening a transport and handling responses; use WSS outside local development. The contract reference documents wire envelopes, validation and failure behavior.
Check that it works
Join with two independent clients, move one, then disconnect and resume it with its session token. The other player's state must remain independent. Send invalid coordinates and a malformed raw message to verify both client and server checks. The real-socket acceptance test covers those sequences, including server rejection that bypasses client validation.
This is not a complete game backend
The example bounds coordinate values; it does not prove a move obeys your game's speed, turn or collision rules. Add authoritative game rules and authentication. Keep session tokens private: possession permits resume and connection takeover. Sessions are capped at 100, expire 30 seconds after disconnect, and are lost on restart. They are not a durable or cross-worker identity store.
Per-connection ordering is not exactly-once delivery. After a disconnect, a client may not know whether its last action completed; reconcile state before retrying side effects. Choose transport limits from measured load and read operations and runtime retry boundaries.
Build and run the complete application
npx --yes redweb@0.13.3 init my-socket --template socket
cd my-socket
npm install --save-exact redweb@0.13.3
npm test
npm run devThe complete socket recipe contains every generated file, its real acceptance tests, and deployment instructions. The source below is one of those files, not a standalone program; initialize the whole project before modifying it.
Source walkthrough: src/handlers.ts
import { randomUUID } from 'node:crypto';
import type { RedWebSocket } from 'redweb';
import { match } from './contract';
class Player {
readonly session = randomUUID();
x = 0;
y = 0;
constructor(readonly name: string) {}
}
function requireUnjoined(socket: RedWebSocket) {
if (socket.context?.session) throw new Error('Already joined.');
}
function currentPlayer(socket: RedWebSocket) {
const session = socket.context?.session as { data?: unknown } | null | undefined;
if (!(session?.data instanceof Player)) throw new Error('Join or resume first.');
return session.data;
}
export const Join = match.handler('join', (socket, { name }, message) => {
requireUnjoined(socket);
const player = new Player(name);
if (!socket.createSession?.(player.session, player)) throw new Error('Session capacity reached.');
return match.send(socket, 'state', player, { requestId: message.requestId });
});
export const Move = match.handler('move', (socket, { x, y }, message) => {
const player = currentPlayer(socket);
player.x = x;
player.y = y;
return match.send(socket, 'state', player, { requestId: message.requestId });
});
export const Resume = match.handler('resume', (socket, { session }, message) => {
requireUnjoined(socket);
if (!(socket.resumeSession?.(session) instanceof Player)) throw new Error('Session expired or unknown.');
return match.send(socket, 'state', currentPlayer(socket), { requestId: message.requestId });
});Complete setup, files and tests →