Skip to content

Controller classes

WebSocketControllerBase subclasses with static deps, contract, and lifecycle methods.

AI generated, pending review Updated 10 days ago · Flare 0.3

Class-based WebSocket endpoints extend WebSocketControllerBase, declare static deps, static state, and optional static contract, then register with host.ws.controller. One instance is built per connection (per wake on a hibernating Durable Object).

import {
FlareHost,
WebSocketControllerBase,
socketContract,
} from "@flare-ts/core";
import { schema, str } from "@flare-ts/lib/schema";
import { node } from "@flare-ts/core/node";
const Msg = schema({ text: str });
const Chat = socketContract({
chat: { incoming: Msg, outgoing: Msg, params: { room: str } },
});
class ChatController extends WebSocketControllerBase<typeof Chat.chat> {
public static override deps = [];
public static override state = [];
public static override contract = Chat.chat;
message(message: { text: string }) {
this.socket.send({ text: `echo: ${message.text}` });
}
}
const host = new FlareHost(node);
host.ws.controller("/chat/:room", ChatController);
const app = host.build();
app.run();
MemberRole
this.socketSend, close, subscribe, and read this.socket.state
this.inputConnect-time params and query (stable for the connection)
this.inject(Token)Resolve registered services from static deps
this.configResolved config sections from static config

Override open, message, close, and error as needed. The message argument is typed from static contract’s incoming schema when declared.

Pass route options as the middle argument for state tokens or a channel selector beyond what the class declares. A controller’s DI is static deps read with this.inject, so inject is not accepted here:

host.ws.controller(
"/chat/:room",
{ contract: Chat.chat, state: [PresenceState] },
ChatController,
);

When the class declares a static contract, options passed here must carry the same contract entry; options without it describe a contract-less route and no longer match the class. When the descriptor is fixed and you need no extra options, use the two-argument form and let static contract carry it.

host.ws.controller() returns a handle carrying the same .upgrade() registrar as inline routes. The hook runs in request scope, before the connection and the controller instance exist, so it lives at the registration site rather than on the class:

host.ws.controller("/chat/:room", ChatController)
.upgrade((upgrade) => {
if (upgrade.header("x-ticket") === undefined) return new FlareResponse(401);
});

The hook declares its own dependencies with the options form, .upgrade({ inject, provides }, handler); the class’s static deps stay connection-time. Values the hook writes to scope.state are readable as this.socket.state from open on. The overview covers the full return contract.

On a Durable Object with hibernation enabled, controller instance fields are discarded on wake. Store per-connection data in this.socket.state, not on this. See Connection state.