WebSocket Component
WebSocketComponent registers a Bun-native WebSocket server on your application by creating and binding a WebSocketServerHelper once the HTTP server is listening.
IMPORTANT
Bun only. binding() throws if the runtime is Node.js. For Node.js support, use the Socket.IO Component instead.
In one example
import { BaseApplication } from '@venizia/ignis';
import { WebSocketComponent, WebSocketBindingKeys } from '@venizia/ignis/websocket';
import { RedisSingleHelper, TWebSocketAuthenticateFn, ValueOrPromise } from '@venizia/ignis-helpers';
export class Application extends BaseApplication {
preConfigure(): ValueOrPromise<void> {
// 1. Redis connection (required, used for cross-instance Pub/Sub)
this.bind({ key: WebSocketBindingKeys.REDIS_CONNECTION }).toValue(
new RedisSingleHelper({ name: 'websocket-redis', host: 'localhost', port: 6379, autoConnect: false }),
);
// 2. Authenticate handler (required, decides accept/reject per client)
const authenticateFn: TWebSocketAuthenticateFn = async data => {
const user = await verifyJWT(data.token as string);
return user ? { userId: user.id } : null;
};
this.bind({ key: WebSocketBindingKeys.AUTHENTICATE_HANDLER }).toValue(authenticateFn);
// 3. Register (binding() validates the two bindings above and defers the rest)
this.component(WebSocketComponent);
}
}WebSocketComponent and WebSocketBindingKeys come from the @venizia/ignis/websocket subpath. They are not exported from the @venizia/ignis root barrel. Helper types (TWebSocketAuthenticateFn, WebSocketServerHelper, WebSocketEmitter, WebSocketDefaults, ...) come from @venizia/ignis-helpers.
How it works
- Two-phase startup solves a timing problem.
binding()runs duringinitialize(), before any Bun server exists. It validates bindings and registers a post-start hook; the actualWebSocketServerHelperis only constructed onceexecutePostStartHooks()runs, afterBun.serve()has produced a live server instance. - Two bindings gate startup.
REDIS_CONNECTION(must be anAbstractRedisHelperinstance) andAUTHENTICATE_HANDLERare required.binding()throws synchronously if either is missing or the wrong type - before the post-start hook is even registered. - Runtime is checked first, fast.
RuntimeModules.detect()runs at the top ofbinding(). On Node.js it throws immediately, so a misconfigured app fails at startup, not on first connection. - The instance appears only after start. The post-start hook binds the configured helper to
WebSocketBindingKeys.WEBSOCKET_INSTANCE. It does not exist during DI construction. Inject it lazily from a service or controller, never via@injectin a constructor. - A custom
fetchhandler splits traffic. After the post-start hook runs,server.reload()swaps in a handler that routes WebSocket upgrade requests (GET <path>with anUpgrade: websocketheader) to Bun's native handler, and everything else to the existing Hono server unchanged.
Common tasks
Customize the server path, rooms, and heartbeat
Bind a partial options object to SERVER_OPTIONS before registering the component. Unset fields fall back to WebSocketDefaults.
this.bind({ key: WebSocketBindingKeys.SERVER_OPTIONS }).toValue({
path: '/realtime',
defaultRooms: ['general', 'announcements'],
heartbeatInterval: 20_000,
heartbeatTimeout: 60_000,
});Add optional lifecycle callbacks
VALIDATE_ROOM_HANDLER, CLIENT_CONNECTED_HANDLER, CLIENT_DISCONNECTED_HANDLER, and MESSAGE_HANDLER are all optional bindings.
this.bind({ key: WebSocketBindingKeys.VALIDATE_ROOM_HANDLER }).toValue(
({ rooms }: { rooms: string[] }) => rooms.filter(room => room.startsWith('public-')),
);WARNING
Without VALIDATE_ROOM_HANDLER bound, every client join request is rejected.
Inject the running instance in a service
WEBSOCKET_INSTANCE is bound after the server starts, so resolve it lazily rather than through the constructor.
private get ws(): WebSocketServerHelper {
return this.application.get<WebSocketServerHelper>({
key: WebSocketBindingKeys.WEBSOCKET_INSTANCE,
});
}Send from a process with no WebSocket server
Background workers, cron jobs, and other microservices use the standalone WebSocketEmitter - it uses the same Redis connection with no local server required.
import { WebSocketEmitter } from '@venizia/ignis-helpers';
const emitter = new WebSocketEmitter({ redisConnection: redisHelper });
await emitter.configure();
await emitter.toRoom({ room: 'dashboard-viewers', event: 'data:update', data: { cpu: 42 } });Require encrypted sessions
Set requireEncryption: true and bind a HANDSHAKE_HANDLER. It becomes required the moment encryption is turned on.
this.bind({ key: WebSocketBindingKeys.SERVER_OPTIONS }).toValue({ requireEncryption: true });
this.bind({ key: WebSocketBindingKeys.HANDSHAKE_HANDLER }).toValue(handshakeFn);See also
- Usage & Examples - injecting the helper,
WebSocketEmitter, wire protocol, client tracking, delivery strategy - Full Reference - lifecycle diagram, binding keys, configuration options,
WebSocketEmitterAPI, internals - Error Reference - error conditions and troubleshooting
- WebSocketServerHelper - the helper this component wires in
- Socket.IO Component - Node.js-compatible alternative
- Bun WebSocket Documentation - official Bun WebSocket API reference
Files:
packages/core/src/components/websocket/component.ts-WebSocketComponentpackages/core/src/components/websocket/common/keys.ts-WebSocketBindingKeyspackages/core/src/components/websocket/common/types.ts-IServerOptions,DEFAULT_SERVER_OPTIONSpackages/core/src/components/websocket/handlers/bun.handler.ts-createBunFetchHandler