Skip to content

Socket.IO -- Setup & Configuration

Real-time, bidirectional, event-based communication using Socket.IO -- with automatic runtime detection for both Node.js and Bun.

Quick Reference

ItemValue
Package@venizia/ignis (core)
ClassSocketIOComponent
Server HelperSocketIOServerHelper
Client HelperSocketIOClientHelper
RuntimesNode.js (@hono/node-server) and Bun (native)
Scaling@socket.io/redis-adapter + @socket.io/redis-emitter

Import Paths

IMPORTANT

SocketIOComponent and SocketIOBindingKeys are not exported from the @venizia/ignis barrel. You must import from the @venizia/ignis/socket-io subpath.

typescript
// From core -- subpath import (NOT from '@venizia/ignis')
import {
  SocketIOComponent,
  SocketIOBindingKeys,
} from '@venizia/ignis/socket-io';

// From helpers -- subpath import
import {
  SocketIOServerHelper,
  SocketIOClientHelper,
  SocketIOConstants,
  SocketIOClientStates,
} from '@venizia/ignis-helpers/socket-io';

// Types from helpers subpath
import type {
  TSocketIOAuthenticateFn,
  TSocketIOValidateRoomFn,
  TSocketIOClientConnectedFn,
  ISocketIOClientOptions,
  IOptions,
  TSocketIOEventHandler,
  TSocketIOClientState,
} from '@venizia/ignis-helpers/socket-io';

Use Cases

  • Live notifications and alerts
  • Real-time chat and messaging
  • Collaborative editing (docs, whiteboards)
  • Live data streams (dashboards, monitoring)
  • Multiplayer game state synchronization
  • Service-to-service real-time communication (via SocketIOClientHelper)

Server Helper Setup

Step 1: Install Dependencies

bash
# Core dependency (already included via @venizia/ignis)
# ioredis is required for the Redis adapter

# For Bun runtime only -- optional peer dependency
bun add @socket.io/bun-engine

Step 2: Bind Required Services

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

typescript
import { BaseApplication } from '@venizia/ignis';
import {
  SocketIOComponent,
  SocketIOBindingKeys,
} from '@venizia/ignis/socket-io';
import { RedisHelper, ValueOrPromise } from '@venizia/ignis-helpers';
import type {
  TSocketIOAuthenticateFn,
  TSocketIOValidateRoomFn,
  TSocketIOClientConnectedFn,
} from '@venizia/ignis-helpers/socket-io';

export class Application extends BaseApplication {
  private redisHelper: RedisHelper;

  preConfigure(): ValueOrPromise<void> {
    this.setupSocketIO();
    // ... other setup
  }

  setupSocketIO() {
    // 1. Redis connection (required for adapter + emitter)
    this.redisHelper = new RedisHelper({
      name: 'socket-io-redis',
      host: process.env.REDIS_HOST ?? 'localhost',
      port: +(process.env.REDIS_PORT ?? 6379),
      password: process.env.REDIS_PASSWORD,
      autoConnect: false,
    });

    this.bind<RedisHelper>({
      key: SocketIOBindingKeys.REDIS_CONNECTION,
    }).toValue(this.redisHelper);

    // 2. Authentication handler (required)
    const authenticateFn: TSocketIOAuthenticateFn = handshake => {
      const token = handshake.headers.authorization;
      // Implement your auth logic -- JWT verification, session check, etc.
      return !!token;
    };

    this.bind<TSocketIOAuthenticateFn>({
      key: SocketIOBindingKeys.AUTHENTICATE_HANDLER,
    }).toValue(authenticateFn);

    // 3. Room validation handler (optional -- joins rejected without this)
    const validateRoomFn: TSocketIOValidateRoomFn = ({ socket, rooms }) => {
      // Return the rooms that the client is allowed to join
      const allowedRooms = rooms.filter(room => room.startsWith('public-'));
      return allowedRooms;
    };

    this.bind<TSocketIOValidateRoomFn>({
      key: SocketIOBindingKeys.VALIDATE_ROOM_HANDLER,
    }).toValue(validateRoomFn);

    // 4. Client connected handler (optional)
    const clientConnectedFn: TSocketIOClientConnectedFn = ({ socket }) => {
      console.log('Client connected:', socket.id);
      // Register custom event handlers on the socket
    };

    this.bind<TSocketIOClientConnectedFn>({
      key: SocketIOBindingKeys.CLIENT_CONNECTED_HANDLER,
    }).toValue(clientConnectedFn);

    // 5. Register the component -- that's it!
    this.component(SocketIOComponent);
  }
}

autoConnect: false Rationale

The RedisHelper is created with autoConnect: false because the server helper internally calls client.duplicate() to create 3 independent Redis connections (pub, sub, emitter). The duplicated clients inherit the lazyConnect setting from the parent. During configure(), the helper detects clients in wait status and explicitly calls client.connect() on each, then awaits all 3 to reach ready status before proceeding. This avoids race conditions where the parent connects before the duplicates are created.

Redis Connection Alternatives

You can use either RedisHelper (single Redis instance) or RedisClusterHelper (Redis Cluster mode). Both extend DefaultRedisHelper, which is the type the component validates against:

typescript
import { RedisClusterHelper } from '@venizia/ignis-helpers';

// For Redis Cluster deployments
const redisHelper = new RedisClusterHelper({
  name: 'socket-io-redis-cluster',
  nodes: [
    { host: 'redis-node-1', port: 6379 },
    { host: 'redis-node-2', port: 6380 },
    { host: 'redis-node-3', port: 6381 },
  ],
  password: process.env.REDIS_PASSWORD,
  autoConnect: false,
});

this.bind<RedisClusterHelper>({
  key: SocketIOBindingKeys.REDIS_CONNECTION,
}).toValue(redisHelper);

The internal TRedisClient type is Redis | Cluster, so both ioredis connection types are supported transparently.

Configuration

Default Server Options

The component applies these defaults if SocketIOBindingKeys.SERVER_OPTIONS is not bound or partially overridden:

OptionDefaultDescription
identifier'SOCKET_IO_SERVER'Unique identifier for the helper instance
path'/io'URL path for Socket.IO handshake/polling
cors.origin'*'Allowed origins (restrict in production!)
cors.methods['GET', 'POST']Allowed HTTP methods for CORS preflight
cors.preflightContinuefalsePass preflight to next handler
cors.optionsSuccessStatus204Status code for successful OPTIONS requests
cors.credentialstrueAllow cookies/auth headers
perMessageDeflate.threshold4096Minimum message size to compress (bytes)
perMessageDeflate.concurrencyLimit20Max concurrent compression operations
perMessageDeflate.clientNoContextTakeovertrueClient releases compression context after each message
perMessageDeflate.serverNoContextTakeovertrueServer releases compression context after each message
perMessageDeflate.serverMaxWindowBits10Server-side maximum window size (2^10 = 1KB)

WARNING

The default cors.origin: '*' is suitable for development only. In production, restrict this to your specific domains.

Full DEFAULT_SERVER_OPTIONS

typescript
const DEFAULT_SERVER_OPTIONS: Partial<IServerOptions> = {
  identifier: 'SOCKET_IO_SERVER',
  path: '/io',
  cors: {
    origin: '*',
    methods: ['GET', 'POST'],
    preflightContinue: false,
    optionsSuccessStatus: 204,
    credentials: true,
  },
  perMessageDeflate: {
    threshold: 4096,
    zlibDeflateOptions: { chunkSize: 10 * 1024 },
    zlibInflateOptions: { windowBits: 12, memLevel: 8 },
    clientNoContextTakeover: true,
    serverNoContextTakeover: true,
    serverMaxWindowBits: 10,
    concurrencyLimit: 20,
  },
};

Custom Configuration

Bind custom server options before registering the component:

typescript
import { SocketIOBindingKeys } from '@venizia/ignis/socket-io';
import type { ServerOptions } from 'socket.io';

const customOptions: Partial<ServerOptions> = {
  path: '/socket.io',
  cors: {
    origin: ['https://myapp.com', 'https://admin.myapp.com'],
    methods: ['GET', 'POST'],
    credentials: true,
  },
  pingTimeout: 60000,
  pingInterval: 25000,
  maxHttpBufferSize: 1e6, // 1MB
};

this.bind<Partial<ServerOptions>>({
  key: SocketIOBindingKeys.SERVER_OPTIONS,
}).toValue(customOptions);

this.component(SocketIOComponent);

NOTE

The identifier field is part of the component's IServerOptions interface (which extends ServerOptions), not Socket.IO's native options. To set the identifier, include it in the bound options object.

Binding Keys

All binding keys are available in SocketIOBindingKeys:

Binding KeyConstantTypeRequiredDefault
@app/socket-io/server-optionsSERVER_OPTIONSPartial<ServerOptions>NoSee defaults above
@app/socket-io/redis-connectionREDIS_CONNECTIONRedisHelper / RedisClusterHelper / DefaultRedisHelperYesnull
@app/socket-io/authenticate-handlerAUTHENTICATE_HANDLERTSocketIOAuthenticateFnYesnull
@app/socket-io/validate-room-handlerVALIDATE_ROOM_HANDLERTSocketIOValidateRoomFnNonull
@app/socket-io/client-connected-handlerCLIENT_CONNECTED_HANDLERTSocketIOClientConnectedFnNonull
@app/socket-io/instanceSOCKET_IO_INSTANCESocketIOServerHelper--Set by component

NOTE

SOCKET_IO_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 Socket.IO.

Constants

Constants are exported from @venizia/ignis-helpers/socket-io and used internally by both the component and the helper.

System Events

ConstantValueDescription
SocketIOConstants.EVENT_PING'ping'Keep-alive ping emitted at pingInterval (default: 30s)
SocketIOConstants.EVENT_CONNECT'connection'New client connected (server-side event)
SocketIOConstants.EVENT_DISCONNECT'disconnect'Client disconnected
SocketIOConstants.EVENT_JOIN'join'Client requests to join room(s)
SocketIOConstants.EVENT_LEAVE'leave'Client requests to leave room(s)
SocketIOConstants.EVENT_AUTHENTICATE'authenticate'Client sends auth credentials
SocketIOConstants.EVENT_AUTHENTICATED'authenticated'Auth success response sent to client
SocketIOConstants.EVENT_UNAUTHENTICATE'unauthenticated'Auth failure response sent to client

Default Rooms

All authenticated clients are automatically joined to these rooms:

ConstantValueDescription
SocketIOConstants.ROOM_DEFAULT'io-default'Default room all authenticated clients join
SocketIOConstants.ROOM_NOTIFICATION'io-notification'Notification broadcast room

TIP

You can override default rooms via the defaultRooms option on SocketIOServerHelper. The component uses the defaults above when not overridden.

Internal Constants (Server Helper)

These constants are defined at module scope in the server helper and are not exported, but they govern default behavior:

ConstantValueDescription
CLIENT_AUTHENTICATE_TIMEOUT10_000 (10s)Time allowed for a client to authenticate before forced disconnect
CLIENT_PING_INTERVAL30_000 (30s)Interval between server-to-client ping emissions

Both can be overridden via the authenticateTimeout and pingInterval constructor options on SocketIOServerHelper.

Client States

Each connected client tracks an authentication state that governs what actions are permitted:

StateConstantDescription
unauthorizedSocketIOClientStates.UNAUTHORIZEDInitial state -- client must emit authenticate within the timeout (default: 10s)
authenticatingSocketIOClientStates.AUTHENTICATINGAuth in progress -- authenticateFn is executing
authenticatedSocketIOClientStates.AUTHENTICATEDAuth successful -- client can send/receive events and join rooms

State Machine Diagram

                    +------------------+
 connect ---------->|  unauthorized    |
                    +--------+---------+
                             | emit('authenticate')
                    +--------v---------+
                    |  authenticating   |
                    +---+----------+---+
            success |              | failure
          +---------v--+   +-------v-----------+
          |authenticated|   |   unauthorized   |--> disconnect
          +-------------+   +------------------+
                                  ^
                            timeout (10s)

SocketIOClientStates Source

typescript
export class SocketIOClientStates {
  static readonly UNAUTHORIZED = 'unauthorized';
  static readonly AUTHENTICATING = 'authenticating';
  static readonly AUTHENTICATED = 'authenticated';

  static readonly SCHEME_SET = new Set([
    this.UNAUTHORIZED,
    this.AUTHENTICATING,
    this.AUTHENTICATED,
  ]);

  static isValid(input: string): input is TConstValue<typeof SocketIOClientStates> {
    return this.SCHEME_SET.has(input);
  }
}

Resolved Bindings

The component resolves all binding keys into a single IResolvedBindings object during the binding() phase:

IResolvedBindings Interface

typescript
interface IResolvedBindings {
  redisConnection: DefaultRedisHelper;
  authenticateFn: TSocketIOAuthenticateFn;
  validateRoomFn?: TSocketIOValidateRoomFn;
  clientConnectedFn?: TSocketIOClientConnectedFn;
}

Callback Type Signatures

typescript
// Called with the socket handshake -- return true to authenticate, false to reject
type TSocketIOAuthenticateFn = (args: IHandshake) => ValueOrPromise<boolean>;

// Called when client emits 'join' -- return the subset of rooms the client is allowed to join
type TSocketIOValidateRoomFn = (opts: {
  socket: IOSocket;
  rooms: string[];
}) => ValueOrPromise<string[]>;

// Called after successful authentication -- register custom event handlers here
type TSocketIOClientConnectedFn = (opts: { socket: IOSocket }) => ValueOrPromise<void>;

IHandshake Interface

typescript
interface IHandshake {
  headers: IncomingHttpHeaders;
  time: string;
  address: string;
  xdomain: boolean;
  secure: boolean;
  issued: number;
  url: string;
  query: ParsedUrlQuery;
  auth: { [key: string]: any };
}

See Also