WebSocket
A Bun-native WebSocket server with post-connection authentication, rooms, heartbeat, and Redis Pub/Sub for horizontal scaling. A Redis-only emitter sends to it from other processes that run no server of their own.
This helper is the raw class: construct it and wire it into your own server yourself. Need it inside an IGNIS app instead? WebSocketComponent does that wiring for you. It creates and binds the helper through DI once your app's Bun server is listening.
IMPORTANT
Bun only. WebSocketServerHelper uses Bun's native WebSocket API and will not run on Node.js. For Node.js, use the Socket.IO Helper instead.
In one example
The smallest working server: authenticate, then send.
import { WebSocketServerHelper } from '@venizia/ignis-helpers';
const helper = new WebSocketServerHelper({
identifier: 'my-ws-server',
server: bunServer, // Bun.Server
redisConnection: redis, // IRedisHelper
authenticateFn: async data => {
const { token } = data as { token: string };
const user = await verifyJWT(token);
return user ? { userId: user.id } : null; // null rejects
},
});
await helper.configure();
bunServer.reload({
fetch: myFetchHandler,
websocket: helper.getBunWebSocketHandler(),
});A client connects, then authenticates before it can send or receive anything else:
const ws = new WebSocket('wss://example.com/ws');
ws.onopen = () => ws.send(JSON.stringify({ event: 'authenticate', data: { token: '...' } }));How it works
- Every connection starts unauthenticated. The WebSocket upgrade always succeeds, and the client starts as
UNAUTHORIZED. It must send{ event: 'authenticate' }withinauthTimeout(5s default) or get closed with code4001. authenticateFndecides accept or reject. On success the client is indexed byuserId, joinsdefaultRooms, and moves toAUTHENTICATED.- Delivery has two tiers - local-only fan-out on this process, or a Redis Pub/Sub layer that also reaches other server instances:
| Tier | Methods | Reach | Mechanism |
|---|---|---|---|
| Local-only | sendToClient, sendToUser, sendToRoom, broadcast | Clients connected to this process | Bun's native server.publish() - O(1) fan-out when possible |
| Cross-instance | send(), WebSocketEmitter | Clients on any server instance | Redis Pub/Sub, via the server's duplicated redisPub/redisSub connections |
- Liveness is passive. The server never pings clients - they must periodically send
{ event: 'heartbeat' }. A sweep everyheartbeatInterval(30s) closes anyone silent for longer thanheartbeatTimeout(90s) with code4002. - Encryption is opt-in per client. An
outboundTransformercallback intercepts every outbound message beforesocket.send(). - Once encrypted, delivery changes. Bun native pub/sub is bypassed for that client.
sendToRoom/broadcastthen fall back to iterating encrypted clients individually.
Common tasks
Send to a client, user, or room (same process)
helper.sendToClient({ clientId: 'abc-123', event: 'notification', data: { message: 'Hello!' } });
helper.sendToUser({ userId: 'user-123', event: 'notification', data: { message: 'New message' } });
helper.sendToRoom({ room: 'ws-notification', event: 'alert', data: { level: 'warning' } });Send across all server instances
Use send() from within the server process, or WebSocketEmitter from a process with no WebSocket server (worker, cron job):
helper.send({
destination: 'ws-notification',
payload: { topic: 'alert', data: { level: 'warning', text: 'CPU high' } },
});import { WebSocketEmitter } from '@venizia/ignis-helpers';
const emitter = new WebSocketEmitter({ identifier: 'cron-emitter', redisConnection: redis });
await emitter.configure();
await emitter.toRoom({ room: 'ws-notification', event: 'alert', data: { level: 'critical' } });Broadcast to everyone
helper.broadcast({ event: 'system:announcement', data: { text: 'Maintenance in 5 min' } });Let clients join rooms
Clients can only join custom rooms when you supply validateRoomFn. Without it, every join request is rejected by default:
const helper = new WebSocketServerHelper({
// ...
validateRoomFn: ({ userId, rooms }) => rooms.filter(room => room.startsWith('public-')),
});// Client-side
ws.send(JSON.stringify({ event: 'join', data: { rooms: ['game-lobby'] } }));React to connect and disconnect
const helper = new WebSocketServerHelper({
// ...
clientConnectedFn: ({ clientId, userId }) => console.log('connected', clientId, userId),
clientDisconnectedFn: ({ clientId, userId }) => console.log('disconnected', clientId, userId),
});See also
- Full reference - every option, method signature, type, and constant
- Socket.IO Helper - Socket.IO-based alternative with Node.js support
- Redis Helper -
RedisSingleHelper/RedisClusterHelperused for cross-instance messaging - Crypto Helper - ECDH key exchange for WebSocket encryption
- WebSocket Component - component-level lifecycle integration
Files:
packages/helpers/src/modules/socket/websocket/server/helper.ts-WebSocketServerHelperpackages/helpers/src/modules/socket/websocket/emitter/helper.ts-WebSocketEmitterpackages/helpers/src/modules/socket/websocket/common/types/- option and callback typespackages/helpers/src/modules/socket/websocket/common/constants.ts-WebSocketEvents,WebSocketChannels,WebSocketDefaults,WebSocketMessageTypes,WebSocketClientStates