Documentation for Redweb 0.15.0. Install that exact version when following these examples.

Server-side TSX for custom socket routes

Added in Redweb 0.15.0 with redweb-client 0.3.0, installed automatically by Redweb. Earlier Redweb 0.14.0/client 0.2.0 packages do not provide this extension. Contributors editing both packages can use the optional npm-link workflow in the repository's docs/CLIENT_DEVELOPMENT.md.

A live page can attach to a registered custom SocketRoute using @page('/', { socket: MatchRoute }). There is no new view class, no second renderer, and no application-specific browser module. Its connection carries ordinary typed commands and reserved redweb:* rendering/completion messages.

Typed controls

defineSocketContract(...).handler(type, callback) still returns a handler class. That class can now be used as a JSX binding:

tsx
<form rw-submit={Join}>
    <input name="room" required />
    <button>Join room</button>
</form>

<button rw-click={Move.with({ cell, revision })}>Move</button>

.with() snapshots JSON data; it never invokes the handler during rendering. TypeScript checks the payload's input type. Incoming wire data is independently validated by the contract, including asynchronous schemas. Form fields merge over the bound object payload. A click sends its bound payload, or an empty object when none was supplied. Bound values are untrusted inputs, not server-held secrets or authorization capabilities. Do not embed private data in them.

Bindings are server-owned identities, not arbitrary function closures. Rendering rejects references to handlers absent from the attached route, even if a different handler has the same message name. Existing string rw-click/rw-submit actions remain unchanged on ordinary live pages. Socket-bound pages dispatch only their custom route handlers; do not mix local @action()/rw-bind commands into them.

Server-owned client state

On an attached connection, socket.page(PageClass) returns that connection's concrete page after checking its class, ownership and active lifetime:

ts
if (socket.page) {
    socket.page(GamePage).game = games.snapshot(room, account);
}

Assigning @state() fields uses the existing reactive renderer and keyed TSX. Raw connections have no page accessor and keep their existing socket protocol. Shared domain state belongs in a room/service; each private page receives only its authorized perspective. Components remain ordinary page-owned Redweb components.

Configuration and boundaries

Register the route once in defineApp({ pages: [GamePage], sockets: [MatchRoute] }). Socket-bound pages must be live, connection-scoped and render TSX. Their route must support protocol version 1 with the default redwebVersion query parameter and explicitly allow duplicate connections (independent tabs). redweb:* handler names are reserved. Page-enabled routes use ordered, bounded message dispatch and track in-flight work for shutdown. JSON commands are supported; binary input is rejected on page-attached connections. Raw route connections retain binary handling.

The adapter preserves route admission, origin and placement policies, then checks the page token, exact route, page identity and page origin. Use authenticate for page identity and authorize for current permissions/session validity. Authorization runs before commands, again after contract validation, and before render publication. Revocation and disconnect cancel ownership even when validation is still running. The page token is not a substitute for credentials.

Initial page attachment completes before custom initialization hooks or commands. Do not call page commands from initialization hooks expecting those hooks themselves to finish first. Each page owns its connection; the same token cannot attach twice.

Reconnect reauthenticates, invokes existing connected() hooks, and sends a fresh rendering snapshot. Game-seat recovery remains in that server hook. Commands are not queued or replayed. Page-session expiration requires a reload; game persistence is not provided by page retention.

Completion and client runtime

After successful handler dispatch, Redweb sends a correlated redweb:result. The generated runtime waits specifically for that terminal type or a protocol error, not intermediate correlated progress messages. Direct client users can opt into the same behavior using client.request(type, payload, { responseType }); omitting the option preserves the existing any-correlated-response behavior.

For an expected application rejection, send a safe correlated error with socket.sendProtocolError('GAME_REJECTED', safeMessage, { requestId }) and return false from the handler. Only literal false declines successful dispatch; undefined remains success. The browser shows error feedback, preserves the form, and keeps its connection usable. Returning false without sending a correlated reply leaves a requesting client waiting until its deadline. Unexpected exceptions retain the existing sanitized handler-failure behavior and close the connection.

This browser support lives in redweb-client's existing feedback/transport modules. The server still owns validation, authorization, game revisions, room fan-out and private rendering. Disabled controls are presentation, never permissions.