WebSocket -- Setup & Configuration
Bun-native real-time, bidirectional communication using pure WebSocket -- with Redis Pub/Sub for horizontal scaling, application-level heartbeat, and post-connection authentication.
IMPORTANT
Bun only. The WebSocket component will throw an error if the runtime is Node.js. For Node.js support, use the Socket.IO Component instead.
Quick Reference
| Item | Value |
|---|---|
| Package | @venizia/ignis (core component) + @venizia/ignis-helpers (helper classes) |
| Component | WebSocketComponent |
| Server Helper | WebSocketServerHelper |
| Emitter Helper | WebSocketEmitter (standalone Redis publisher) |
| Runtimes | Bun only (throws on Node.js) |
| Scaling | Redis Pub/Sub (ioredis -- single or Cluster) |
Import Paths
IMPORTANT
WebSocketComponent and WebSocketBindingKeys are not exported from the @venizia/ignis barrel. You must import from the @venizia/ignis/websocket subpath.
// From core -- subpath import (NOT from '@venizia/ignis')
import {
WebSocketComponent,
WebSocketBindingKeys,
} from '@venizia/ignis/websocket';
// From helpers -- types, helpers, constants (exported from main entry)
import {
WebSocketServerHelper,
WebSocketEmitter,
WebSocketDefaults,
WebSocketEvents,
WebSocketChannels,
WebSocketClientStates,
WebSocketMessageTypes,
} from '@venizia/ignis-helpers';
import type {
IWebSocketServerOptions,
IWebSocketEmitterOptions,
IWebSocketClient,
IWebSocketMessage,
IRedisSocketMessage,
IBunWebSocketConfig,
TWebSocketAuthenticateFn,
TWebSocketValidateRoomFn,
TWebSocketClientConnectedFn,
TWebSocketClientDisconnectedFn,
TWebSocketMessageHandler,
TWebSocketOutboundTransformer,
TWebSocketHandshakeFn,
} from '@venizia/ignis-helpers';NOTE
IServerOptions (the core component's subset type) is not exported from @venizia/ignis or @venizia/ignis/websocket. Only WebSocketBindingKeys and WebSocketComponent are exported from the core subpath. All helper types, constants, and classes are imported from @venizia/ignis-helpers.
Use Cases
- Live notifications and alerts
- Real-time chat and messaging
- Collaborative editing (docs, whiteboards)
- Live data streams (dashboards, monitoring)
- Multiplayer game state synchronization
- IoT device communication
- Background job progress updates (via
WebSocketEmitter) - Cross-service event broadcasting (via
WebSocketEmitter)
Setup
Step 1: Install Dependencies
# Core dependency (already included via @venizia/ignis)
# ioredis is required for Redis Pub/Sub
bun add ioredisStep 2: Bind Required Services
In your application's preConfigure() method, bind the required services and register the component:
Full Setup Example
import { BaseApplication } from '@venizia/ignis';
import {
WebSocketComponent,
WebSocketBindingKeys,
} from '@venizia/ignis/websocket';
import {
RedisHelper,
} from '@venizia/ignis-helpers';
import type {
TWebSocketAuthenticateFn,
TWebSocketValidateRoomFn,
TWebSocketClientConnectedFn,
TWebSocketClientDisconnectedFn,
TWebSocketMessageHandler,
TWebSocketOutboundTransformer,
TWebSocketHandshakeFn,
IBunWebSocketConfig,
ValueOrPromise,
} from '@venizia/ignis-helpers';
export class Application extends BaseApplication {
private redisHelper: RedisHelper;
preConfigure(): ValueOrPromise<void> {
this.setupWebSocket();
// ... other setup
}
setupWebSocket() {
// 1. Redis connection (required for cross-instance messaging)
this.redisHelper = new RedisHelper({
name: 'websocket-redis',
host: process.env.REDIS_HOST ?? 'localhost',
port: +(process.env.REDIS_PORT ?? 6379),
password: process.env.REDIS_PASSWORD,
autoConnect: false,
});
this.bind<RedisHelper>({
key: WebSocketBindingKeys.REDIS_CONNECTION,
}).toValue(this.redisHelper);
// 2. Authentication handler (required)
const authenticateFn: TWebSocketAuthenticateFn = async (data) => {
const token = data.token as string;
if (!token) return null;
const user = await verifyJWT(token);
if (!user) return null;
return { userId: user.id, metadata: { role: user.role } };
};
this.bind<TWebSocketAuthenticateFn>({
key: WebSocketBindingKeys.AUTHENTICATE_HANDLER,
}).toValue(authenticateFn);
// 3. Room validation handler (optional -- joins rejected without this)
const validateRoomFn: TWebSocketValidateRoomFn = ({ clientId, userId, rooms }) => {
return rooms.filter(room => room.startsWith('public-'));
};
this.bind<TWebSocketValidateRoomFn>({
key: WebSocketBindingKeys.VALIDATE_ROOM_HANDLER,
}).toValue(validateRoomFn);
// 4. Client connected handler (optional)
const clientConnectedFn: TWebSocketClientConnectedFn = ({ clientId, userId }) => {
console.log('Client connected:', clientId, userId);
};
this.bind<TWebSocketClientConnectedFn>({
key: WebSocketBindingKeys.CLIENT_CONNECTED_HANDLER,
}).toValue(clientConnectedFn);
// 5. Client disconnected handler (optional)
const clientDisconnectedFn: TWebSocketClientDisconnectedFn = ({ clientId, userId }) => {
console.log('Client disconnected:', clientId, userId);
};
this.bind<TWebSocketClientDisconnectedFn>({
key: WebSocketBindingKeys.CLIENT_DISCONNECTED_HANDLER,
}).toValue(clientDisconnectedFn);
// 6. Message handler (optional -- for custom events)
const messageHandler: TWebSocketMessageHandler = ({ clientId, userId, message }) => {
console.log('Custom event:', message.event, message.data);
};
this.bind<TWebSocketMessageHandler>({
key: WebSocketBindingKeys.MESSAGE_HANDLER,
}).toValue(messageHandler);
// 7. Outbound transformer (optional -- for per-client encryption)
const outboundTransformer: TWebSocketOutboundTransformer = async ({ client, event, data }) => {
if (!client.encrypted) return null;
// Encrypt using client's derived AES key (from ECDH handshake)
const encrypted = await encryptForClient(client.id, JSON.stringify({ event, data }));
return { event: 'encrypted', data: encrypted };
};
this.bind<TWebSocketOutboundTransformer>({
key: WebSocketBindingKeys.OUTBOUND_TRANSFORMER,
}).toValue(outboundTransformer);
// 8. Handshake handler (optional -- required when requireEncryption is true)
const handshakeFn: TWebSocketHandshakeFn = async ({ clientId, data }) => {
const clientPubKey = data.publicKey as string;
if (!clientPubKey) return null; // Reject -- no public key provided
const salt = crypto.getRandomValues(new Uint8Array(32));
const saltB64 = Buffer.from(salt).toString('base64');
const aesKey = await deriveSharedSecret(clientPubKey, salt);
storeClientKey(clientId, aesKey);
return { serverPublicKey: serverPublicKeyB64, salt: saltB64 };
};
this.bind<TWebSocketHandshakeFn>({
key: WebSocketBindingKeys.HANDSHAKE_HANDLER,
}).toValue(handshakeFn);
// 9. Server options (optional -- customize defaults)
this.bind({
key: WebSocketBindingKeys.SERVER_OPTIONS,
}).toValue({
identifier: 'my-app-websocket',
requireEncryption: true,
});
// 10. Register the component
this.component(WebSocketComponent);
}
}Configuration
The core component's IServerOptions interface controls the WebSocket server setup. Default values come from DEFAULT_SERVER_OPTIONS and WebSocketDefaults:
{
identifier: 'WEBSOCKET_SERVER',
path: '/ws', // WebSocketDefaults.PATH
defaultRooms: [ // Joined automatically after auth
'ws-default', // WebSocketDefaults.ROOM
'ws-notification', // WebSocketDefaults.NOTIFICATION_ROOM
],
heartbeatInterval: 30000, // 30 seconds (WebSocketDefaults.HEARTBEAT_INTERVAL)
heartbeatTimeout: 90000, // 90 seconds (WebSocketDefaults.HEARTBEAT_TIMEOUT)
requireEncryption: false,
serverOptions: { // Bun native WebSocket config (IBunWebSocketConfig)
sendPings: true, // WebSocketDefaults.SEND_PINGS
idleTimeout: 60, // WebSocketDefaults.IDLE_TIMEOUT (seconds)
maxPayloadLength: 131072, // WebSocketDefaults.MAX_PAYLOAD_LENGTH (128 KB)
},
}NOTE
The DEFAULT_SERVER_OPTIONS in the core component only sets identifier and path. The remaining defaults (defaultRooms, heartbeatInterval, heartbeatTimeout, serverOptions) come from WebSocketDefaults applied by the helper constructor.
To customize options, bind a partial options object before registering the component:
Custom Server Options Example
import { WebSocketBindingKeys } from '@venizia/ignis/websocket';
this.bind({
key: WebSocketBindingKeys.SERVER_OPTIONS,
}).toValue({
identifier: 'my-app-websocket',
path: '/realtime',
defaultRooms: ['general', 'announcements'], // Override default rooms
heartbeatInterval: 20000, // More frequent heartbeats
heartbeatTimeout: 60000, // Shorter timeout
requireEncryption: true, // Require ECDH handshake
serverOptions: {
maxPayloadLength: 2097152, // 2 MB max payload
backpressureLimit: 2097152, // 2 MB backpressure limit
},
});NOTE
authTimeout and encryptedBatchLimit are properties of the helper's IWebSocketServerOptions, not the core component's IServerOptions. The component uses the helper defaults for those (5000 ms and 10 respectively). If you need to customize them, you must set them on the helper directly (not via binding keys).
WebSocketDefaults Constants
All tunable defaults are defined in the WebSocketDefaults class. The helper falls back to these when no explicit value is provided.
| Constant | Value | Description |
|---|---|---|
PATH | '/ws' | Default WebSocket endpoint path |
ROOM | 'ws-default' | Default room name |
NOTIFICATION_ROOM | 'ws-notification' | Default notification room name |
BROADCAST_TOPIC | 'ws:internal:broadcast' | Internal Bun pub/sub broadcast topic |
MAX_PAYLOAD_LENGTH | 131072 (128 KB) | Maximum message payload size |
IDLE_TIMEOUT | 60 | Bun idle timeout in seconds |
BACKPRESSURE_LIMIT | 1048576 (1 MB) | Bun backpressure limit |
SEND_PINGS | true | Enable WebSocket pings |
PUBLISH_TO_SELF | false | Whether server receives its own publishes |
AUTH_TIMEOUT | 5000 (5 s) | Time to authenticate before disconnect |
HEARTBEAT_INTERVAL | 30000 (30 s) | Interval between heartbeat sweeps |
HEARTBEAT_TIMEOUT | 90000 (90 s) | Disconnect after 3 missed heartbeats |
ENCRYPTED_BATCH_LIMIT | 10 | Max concurrent encryption operations |
TIP
MAX_PAYLOAD_LENGTH, IDLE_TIMEOUT, BACKPRESSURE_LIMIT, SEND_PINGS, and PUBLISH_TO_SELF are Bun-native WebSocket settings passed via serverOptions inside IServerOptions. The rest are application-level settings on IWebSocketServerOptions (the helper constructor options).
Full IBunWebSocketConfig Interface
/** Bun WebSocket native configuration options */
interface IBunWebSocketConfig {
perMessageDeflate?: boolean;
maxPayloadLength?: number; // Default: 128 KB (131072)
idleTimeout?: number; // Default: 60 s
backpressureLimit?: number; // Default: 1 MB (1048576)
closeOnBackpressureLimit?: boolean;
sendPings?: boolean; // Default: true
publishToSelf?: boolean; // Default: false
}These options are passed directly to Bun's native WebSocket handler. Set them via serverOptions inside the options bound to WebSocketBindingKeys.SERVER_OPTIONS.
Full IServerOptions Interface (Core Component)
interface IServerOptions {
identifier: string; // Default: 'WEBSOCKET_SERVER'
path?: string; // Default: '/ws' (from WebSocketDefaults.PATH)
defaultRooms?: string[]; // Default: ['ws-default', 'ws-notification']
serverOptions?: IBunWebSocketConfig; // Bun native WebSocket config
heartbeatInterval?: number; // Default: 30000 (30 s)
heartbeatTimeout?: number; // Default: 90000 (90 s)
requireEncryption?: boolean; // Default: false
}NOTE
IServerOptions is the core component's options type. It is a subset of the helper's IWebSocketServerOptions, which additionally includes server, redisConnection, callback functions, authTimeout, and encryptedBatchLimit. The component fills in those extra fields from the DI container before constructing the helper.
Binding Keys
| Binding Key | Constant | Type | Required | Default |
|---|---|---|---|---|
@app/websocket/server-options | WebSocketBindingKeys.SERVER_OPTIONS | Partial<IServerOptions> | No | See Configuration |
@app/websocket/redis-connection | WebSocketBindingKeys.REDIS_CONNECTION | DefaultRedisHelper | Yes | null |
@app/websocket/authenticate-handler | WebSocketBindingKeys.AUTHENTICATE_HANDLER | TWebSocketAuthenticateFn | Yes | null |
@app/websocket/validate-room-handler | WebSocketBindingKeys.VALIDATE_ROOM_HANDLER | TWebSocketValidateRoomFn | No | null |
@app/websocket/client-connected-handler | WebSocketBindingKeys.CLIENT_CONNECTED_HANDLER | TWebSocketClientConnectedFn | No | null |
@app/websocket/client-disconnected-handler | WebSocketBindingKeys.CLIENT_DISCONNECTED_HANDLER | TWebSocketClientDisconnectedFn | No | null |
@app/websocket/message-handler | WebSocketBindingKeys.MESSAGE_HANDLER | TWebSocketMessageHandler | No | null |
@app/websocket/outbound-transformer | WebSocketBindingKeys.OUTBOUND_TRANSFORMER | TWebSocketOutboundTransformer | No | null |
@app/websocket/handshake-handler | WebSocketBindingKeys.HANDSHAKE_HANDLER | TWebSocketHandshakeFn | No* | null |
@app/websocket/instance | WebSocketBindingKeys.WEBSOCKET_INSTANCE | WebSocketServerHelper | -- | Set by component |
NOTE
HANDSHAKE_HANDLER is required when IServerOptions.requireEncryption is true. It performs ECDH key exchange during authentication.
NOTE
WEBSOCKET_INSTANCE is not set by you -- the component creates and binds it automatically after the server starts. Inject it in services/controllers to interact with WebSocket.
Callback Type Signatures
| Binding Key | Callback Type | Required | Description |
|---|---|---|---|
AUTHENTICATE_HANDLER | TWebSocketAuthenticateFn | Yes | Returns { userId, metadata } or null/false to reject |
VALIDATE_ROOM_HANDLER | TWebSocketValidateRoomFn | No | Filters requested rooms, returns allowed rooms |
CLIENT_CONNECTED_HANDLER | TWebSocketClientConnectedFn | No | Called after successful authentication |
CLIENT_DISCONNECTED_HANDLER | TWebSocketClientDisconnectedFn | No | Called on disconnect (after cleanup) |
MESSAGE_HANDLER | TWebSocketMessageHandler | No | Handles non-system messages from authenticated clients |
OUTBOUND_TRANSFORMER | TWebSocketOutboundTransformer | No | Transforms outbound messages (e.g., per-client encryption) |
HANDSHAKE_HANDLER | TWebSocketHandshakeFn | When requireEncryption: true | Returns { serverPublicKey, salt } or null/false to reject |
TWebSocketAuthenticateFn
type TWebSocketAuthenticateFn<
AuthDataType extends Record<string, unknown> = Record<string, unknown>,
MetadataType extends Record<string, unknown> = Record<string, unknown>,
> = (
opts: AuthDataType,
) => ValueOrPromise<{ userId?: string; metadata?: MetadataType } | null | false>;Receives the data field from the client's authenticate event. Return { userId, metadata } on success, or null/false to reject (closes with code 4003).
TWebSocketValidateRoomFn
type TWebSocketValidateRoomFn = (opts: {
clientId: string;
userId?: string;
rooms: string[];
}) => ValueOrPromise<string[]>;Called when a client sends a join event. Receives the sanitized room list (internal ws: prefix rooms are already filtered out). Return the subset of rooms the client is allowed to join.
WARNING
If no validateRoomFn is bound, all join requests are rejected. You must bind this handler if you want clients to join custom rooms.
TWebSocketClientConnectedFn
type TWebSocketClientConnectedFn<
MetadataType extends Record<string, unknown> = Record<string, unknown>,
> = (opts: {
clientId: string;
userId?: string;
metadata?: MetadataType;
}) => ValueOrPromise<void>;Called after a client has been fully authenticated, joined default rooms, and received the connected event. Errors thrown here are caught and logged -- they do not disconnect the client.
TWebSocketClientDisconnectedFn
type TWebSocketClientDisconnectedFn = (opts: {
clientId: string;
userId?: string;
}) => ValueOrPromise<void>;Called after internal cleanup (auth timer cleared, removed from user/room indexes, removed from clients map). Errors thrown here are caught and logged.
TWebSocketMessageHandler
type TWebSocketMessageHandler = (opts: {
clientId: string;
userId?: string;
message: IWebSocketMessage;
}) => ValueOrPromise<void>;Called for any message from an authenticated client whose event is not a system event (authenticate, connected, disconnect, join, leave, error, heartbeat, encrypted). If no handler is bound, non-system messages are silently dropped.
TWebSocketOutboundTransformer
type TWebSocketOutboundTransformer<
DataType = unknown,
MetadataType extends Record<string, unknown> = Record<string, unknown>,
> = (opts: {
client: IWebSocketClient<MetadataType>;
event: string;
data: DataType;
}) => ValueOrPromise<TNullable<{ event: string; data: DataType }>>;Intercepts every outbound message to encrypted clients only before socket.send(). Return null to send the original { event, data } unchanged, or return a transformed { event, data } (e.g., { event: 'encrypted', data: ciphertext }).
NOTE
The transformer is only called for clients where client.encrypted === true. Non-encrypted clients bypass this entirely (zero overhead).
TWebSocketHandshakeFn
type TWebSocketHandshakeFn<
AuthDataType extends Record<string, unknown> = Record<string, unknown>,
> = (opts: {
clientId: string;
userId?: string;
data: AuthDataType;
}) => ValueOrPromise<{ serverPublicKey: string; salt: string } | null | false>;Called during authentication when requireEncryption is true. Receives the same data payload as authenticateFn. Return { serverPublicKey, salt } on success -- these are included in the connected event sent to the client. Return null/false to reject (closes with code 4004).
See Also
- Usage & Examples - Server-side usage, emitter, wire protocol, client tracking, and delivery strategy
- API Reference - Architecture, WebSocketEmitter API, and internals
- Error Reference - Error conditions table and troubleshooting
- WebSocketServerHelper - Helper API documentation
- Socket.IO Component - Node.js-compatible alternative with Socket.IO
- Bun WebSocket Documentation - Official Bun WebSocket API reference