WebSocket Concepts
The PXL Framework's WebSocket module provides real-time, bidirectional communication between server and clients with enterprise features like room-based messaging, authentication, and multi-worker coordination.
Core Concepts
Message-Based Architecture
WebSocket communication is organized around structured messages with a consistent protocol:
{
type: string; // Message category (e.g., 'chat', 'notification', 'system')
action: string; // Specific action (e.g., 'send', 'update', 'delete')
data?: any; // Optional payload
}This structure enables:
- Controller-based routing: Messages are routed to specific controller methods based on
typeandaction - Type safety: Predictable message format for both client and server
- Scalability: Easy to add new message types without breaking existing code
Client Management
The WebSocket server maintains a registry of connected clients with:
- Unique Client IDs: Auto-generated identifier for each connection
- User Metadata: Store authenticated user info (from JWT) and custom data
- Connection State: Track connection status and last activity
- WebSocket Reference: Direct access to underlying WebSocket connection (when on same worker)
// Client structure
{
clientId: string;
ws: WebSocket | null; // null if on different worker
lastActivity: number;
user?: {
userId: number;
payload: Record<string, unknown>;
};
roomName?: string | null;
// ... custom metadata
}Room-Based Messaging
Rooms group clients together for targeted broadcasts, similar to channels or topics:
Room: "chat:general"
├── Client A (User 1)
├── Client B (User 2)
└── Client C (User 3)
Room: "game:lobby:5"
├── Client D (User 4)
└── Client E (User 5)Key Features:
- Clients can join/leave rooms dynamically
- Broadcast messages to all room members
- Room membership synced across workers via Redis
- Optional multi-room support (clients in multiple rooms simultaneously)
Multi-Worker Coordination
In cluster mode, WebSocket connections are distributed across workers. Redis pub/sub enables coordination:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Worker 1 │ │ Worker 2 │ │ Worker 3 │
│ Client A │ │ Client B │ │ Client C │
│ Client D │ │ Client E │ │ Client F │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└─────────────────────┼─────────────────────┘
│
┌──────▼──────┐
│ Redis │
│ Pub/Sub │
└─────────────┘How it works:
- Worker 1 receives a message from Client A
- Worker 1 publishes to Redis with event type
SendMessageToAll - All workers (1, 2, 3) receive the Redis event
- Each worker broadcasts to its local clients (A, D on Worker 1; B, E on Worker 2; C, F on Worker 3)
Synchronized Events:
- Client connect/disconnect notifications
- Room join/leave operations
- Cross-worker broadcasts
- Client metadata updates
Controller-Based Routing
Messages are routed to controller methods based on type and action:
// Route definition
{
type: 'chat',
action: 'send',
controllerName: 'chat',
controller: ChatController
}
// Incoming message
{
type: 'chat',
action: 'send',
data: { text: 'Hello!' }
}
// Routes to: ChatController.send()Controllers inherit from WebSocketServerBaseController and have access to:
webSocketServer: Send messages, manage clientsredisInstance: Direct Redis accessqueueManager: Enqueue background jobsdatabaseInstance: Database operations
Authentication Flow
JWT tokens authenticate WebSocket connections:
1. Client requests: ws://server/ws?token=<jwt>
2. Server validates JWT using configured secret
3. Extract user info from token (userId from 'sub' claim)
4. Store in client metadata: { userId, payload }
5. If invalid → reject with 401 UnauthorizedAuthenticated user info is available in all controller methods:
const client = this.webSocketServer.clientManager.getClient({ clientId });
const userId = client?.user?.userId;
const userRole = client?.user?.payload?.role;Lifecycle Management
WebSocket servers follow the application lifecycle:
1. Configure: Load routes and controllers
2. Start: Initialize WebSocket server, subscribe to Redis events
3. Runtime: Handle connections, route messages, coordinate across workers
4. Shutdown: Close connections gracefully, unsubscribe from Redis, cleanupGraceful Shutdown:
- Stop accepting new connections
- Close all client WebSocket connections
- Unsubscribe from all Redis events
- Clean up intervals and timers
- Reset client and room managers
Inactive Client Management
Automatically clean up abandoned connections:
- Track
lastActivitytimestamp per client (updated on each message) - Periodic check runs at configured interval (e.g., every 60 seconds)
- Clients inactive longer than threshold (e.g., 5 minutes) are disconnected
- Frees server resources and prevents memory leaks
Performance Monitoring
WebSocket health is tracked in the performance monitoring system:
- Active Connections: Current connected client count
- Rooms: Number of active rooms
- Message Throughput: Messages per second
- Connection Events: Connect/disconnect rates
Architecture Patterns
Event-Driven Communication
// Server broadcasts event
websocket.sendMessageToAll({
data: {
type: 'user',
action: 'statusChanged',
data: { userId: 123, status: 'online' },
},
});
// Clients listen and react
ws.onmessage = event => {
const msg = JSON.parse(event.data);
if (msg.type === 'user' && msg.action === 'statusChanged') {
updateUserStatus(msg.data.userId, msg.data.status);
}
};Request-Response Pattern
// Client sends request
ws.send(
JSON.stringify({
type: 'user',
action: 'getProfile',
data: { userId: 123 },
}),
);
// Server controller processes and responds
export default class UserController extends WebSocketServerBaseController {
public async getProfile(ws: WebSocket, clientId: string, data: any) {
const profile = await this.fetchUserProfile(data.userId);
// Response sent back to requesting client
return {
success: true,
profile,
};
}
}Publish-Subscribe with Rooms
// Clients subscribe by joining room
ws.send(
JSON.stringify({
type: 'system',
action: 'joinRoom',
data: { roomName: 'stock:AAPL' },
}),
);
// Server publishes updates to room subscribers
const room = websocket.rooms.get('stock:AAPL');
room?.forEach(clientId => {
const client = websocket.clientManager.getClient({ clientId });
if (client?.ws) {
websocket.sendClientMessage(client.ws, {
type: 'stock',
action: 'priceUpdate',
data: { symbol: 'AAPL', price: 150.25 },
});
}
});Design Principles
- Separation of Concerns: Controllers handle business logic, managers handle infrastructure
- Scalability First: Redis pub/sub enables horizontal scaling across workers
- Type Safety: Structured messages with consistent protocol
- Fault Tolerance: Graceful degradation, automatic cleanup, error handling
- Developer Experience: Intuitive API, comprehensive events, debugging support
Common Use Cases
- Chat Applications: Real-time messaging with rooms and presence
- Live Updates: Dashboard metrics, notifications, feed updates
- Collaborative Editing: Simultaneous document editing
- Gaming: Multiplayer game state synchronization
- IoT/Monitoring: Device status streaming, sensor data
- Trading/Finance: Live market data, order updates
Learn More
- WebSocket Guide - Comprehensive implementation guide
- WebSocket API Reference - Complete API documentation
- Hello World Example - Working example with Vue 3 frontend