Skip to content

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

ItemValue
Package@venizia/ignis (core component) + @venizia/ignis-helpers (helper classes)
ComponentWebSocketComponent
Server HelperWebSocketServerHelper
Emitter HelperWebSocketEmitter (standalone Redis publisher)
RuntimesBun only (throws on Node.js)
ScalingRedis 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.

typescript
// 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

bash
# Core dependency (already included via @venizia/ignis)
# ioredis is required for Redis Pub/Sub
bun add ioredis

Step 2: Bind Required Services

In your application's preConfigure() method, bind the required services and register the component:

Full Setup Example

typescript
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:

typescript
{
  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

typescript
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.

ConstantValueDescription
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_LENGTH131072 (128 KB)Maximum message payload size
IDLE_TIMEOUT60Bun idle timeout in seconds
BACKPRESSURE_LIMIT1048576 (1 MB)Bun backpressure limit
SEND_PINGStrueEnable WebSocket pings
PUBLISH_TO_SELFfalseWhether server receives its own publishes
AUTH_TIMEOUT5000 (5 s)Time to authenticate before disconnect
HEARTBEAT_INTERVAL30000 (30 s)Interval between heartbeat sweeps
HEARTBEAT_TIMEOUT90000 (90 s)Disconnect after 3 missed heartbeats
ENCRYPTED_BATCH_LIMIT10Max 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

typescript
/** 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)

typescript
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 KeyConstantTypeRequiredDefault
@app/websocket/server-optionsWebSocketBindingKeys.SERVER_OPTIONSPartial<IServerOptions>NoSee Configuration
@app/websocket/redis-connectionWebSocketBindingKeys.REDIS_CONNECTIONDefaultRedisHelperYesnull
@app/websocket/authenticate-handlerWebSocketBindingKeys.AUTHENTICATE_HANDLERTWebSocketAuthenticateFnYesnull
@app/websocket/validate-room-handlerWebSocketBindingKeys.VALIDATE_ROOM_HANDLERTWebSocketValidateRoomFnNonull
@app/websocket/client-connected-handlerWebSocketBindingKeys.CLIENT_CONNECTED_HANDLERTWebSocketClientConnectedFnNonull
@app/websocket/client-disconnected-handlerWebSocketBindingKeys.CLIENT_DISCONNECTED_HANDLERTWebSocketClientDisconnectedFnNonull
@app/websocket/message-handlerWebSocketBindingKeys.MESSAGE_HANDLERTWebSocketMessageHandlerNonull
@app/websocket/outbound-transformerWebSocketBindingKeys.OUTBOUND_TRANSFORMERTWebSocketOutboundTransformerNonull
@app/websocket/handshake-handlerWebSocketBindingKeys.HANDSHAKE_HANDLERTWebSocketHandshakeFnNo*null
@app/websocket/instanceWebSocketBindingKeys.WEBSOCKET_INSTANCEWebSocketServerHelper--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 KeyCallback TypeRequiredDescription
AUTHENTICATE_HANDLERTWebSocketAuthenticateFnYesReturns { userId, metadata } or null/false to reject
VALIDATE_ROOM_HANDLERTWebSocketValidateRoomFnNoFilters requested rooms, returns allowed rooms
CLIENT_CONNECTED_HANDLERTWebSocketClientConnectedFnNoCalled after successful authentication
CLIENT_DISCONNECTED_HANDLERTWebSocketClientDisconnectedFnNoCalled on disconnect (after cleanup)
MESSAGE_HANDLERTWebSocketMessageHandlerNoHandles non-system messages from authenticated clients
OUTBOUND_TRANSFORMERTWebSocketOutboundTransformerNoTransforms outbound messages (e.g., per-client encryption)
HANDSHAKE_HANDLERTWebSocketHandshakeFnWhen requireEncryption: trueReturns { serverPublicKey, salt } or null/false to reject

TWebSocketAuthenticateFn

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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