Overview
Voxora’s real-time chat system is built on Socket.IO , providing bidirectional, event-based communication between clients and servers. This enables instant message delivery, typing indicators, presence updates, and more.
Architecture
Connection Flow
1. Client Connection
Clients connect to the Socket.IO server with authentication:
import io from 'socket.io-client' ;
const socket = io ( 'http://localhost:3002' , {
auth: {
token: 'jwt-token-here'
},
transports: [ 'websocket' , 'polling' ]
});
socket . on ( 'connect' , () => {
console . log ( 'Connected:' , socket . id );
});
2. Authentication
Token Validation
Server validates JWT token from handshake
User Loading
Load user data from database
Room Assignment
Join user to appropriate rooms (conversations, departments)
Presence Update
Update user online status in Redis
Core Events
Message Events
send_message
new_message
message_delivered
Client → Server : Send a new messagesocket . emit ( 'send_message' , {
conversationId: '123' ,
content: 'Hello!' ,
type: 'text' // or 'file', 'image'
});
Server → Client : Receive a new messagesocket . on ( 'new_message' , ( message ) => {
console . log ( 'New message:' , message );
// Update UI
});
Server → Client : Message delivery confirmationsocket . on ( 'message_delivered' , ( messageId ) => {
// Update message status to delivered
});
Typing Indicators
// User starts typing
socket . emit ( 'typing_start' , {
conversationId: '123'
});
// User stops typing
socket . emit ( 'typing_stop' , {
conversationId: '123'
});
// Listen for others typing
socket . on ( 'user_typing' , ({ userId , userName }) => {
showTypingIndicator ( userName );
});
Presence Events
Track when users come online or go offline:
socket . on ( 'user_online' , ({ userId }) => {
updateUserStatus ( userId , 'online' );
});
socket . on ( 'user_offline' , ({ userId }) => {
updateUserStatus ( userId , 'offline' );
});
Rooms & Namespaces
Conversation Rooms
Each conversation has its own room for message broadcasting:
// Join a conversation
socket . on ( 'join_conversation' , async ({ conversationId }) => {
await socket . join ( conversationId );
// Send only to this room
io . to ( conversationId ). emit ( 'user_joined' , {
userId: socket . userId
});
});
// Leave a conversation
socket . on ( 'leave_conversation' , ({ conversationId }) => {
socket . leave ( conversationId );
});
Department Namespaces
Organize agents by department:
const departmentNamespace = io . of ( '/department' );
departmentNamespace . on ( 'connection' , ( socket ) => {
const { departmentId } = socket . handshake . query ;
socket . join ( `dept_ ${ departmentId } ` );
});
Message Persistence
Messages are saved to MongoDB before being broadcast:
socket . on ( 'send_message' , async ( data ) => {
try {
// 1. Create message in database
const message = await Message . create ({
conversationId: data . conversationId ,
senderId: socket . userId ,
content: data . content ,
type: data . type ,
timestamp: new Date ()
});
// 2. Broadcast to conversation room
io . to ( data . conversationId ). emit ( 'new_message' , message );
// 3. Send delivery confirmation
socket . emit ( 'message_delivered' , message . _id );
// 4. Update conversation's last message
await Conversation . findByIdAndUpdate (
data . conversationId ,
{ lastMessage: message . _id , updatedAt: new Date () }
);
} catch ( error ) {
socket . emit ( 'message_error' , {
error: error . message
});
}
});
Scalability with Redis
For multi-server deployments, use the Redis adapter:
import { createAdapter } from '@socket.io/redis-adapter' ;
import { createClient } from 'redis' ;
const pubClient = createClient ({ url: 'redis://localhost:6379' });
const subClient = pubClient . duplicate ();
await Promise . all ([ pubClient . connect (), subClient . connect ()]);
io . adapter ( createAdapter ( pubClient , subClient ));
This allows Socket.IO instances across multiple servers to communicate via Redis pub/sub.
Error Handling
Implement robust error handling for connection issues:
socket . on ( 'connect_error' , ( error ) => {
console . error ( 'Connection error:' , error );
// Show reconnection UI
});
socket . on ( 'reconnect' , ( attemptNumber ) => {
console . log ( 'Reconnected after' , attemptNumber , 'attempts' );
// Rejoin rooms, sync state
});
socket . on ( 'reconnect_failed' , () => {
console . error ( 'Failed to reconnect' );
// Show offline mode
});
1. Message Batching
Batch multiple messages to reduce events:
const messageBuffer = [];
const BATCH_SIZE = 10 ;
const BATCH_INTERVAL = 100 ; // ms
socket . on ( 'send_message' , ( message ) => {
messageBuffer . push ( message );
if ( messageBuffer . length >= BATCH_SIZE ) {
flushMessages ();
}
});
setInterval (() => {
if ( messageBuffer . length > 0 ) {
flushMessages ();
}
}, BATCH_INTERVAL );
2. Binary Data
Use binary transmission for file uploads:
// Client
socket . emit ( 'file_upload' , fileBuffer );
// Server
socket . on ( 'file_upload' , ( buffer ) => {
// Handle binary data
});
3. Compression
Enable per-message deflate compression:
const io = new Server ( server , {
perMessageDeflate: {
threshold: 1024 // Only compress messages > 1KB
}
});
Security Best Practices
Authentication Always verify JWT tokens on connection
Authorization Check user permissions before joining rooms
Input Validation Validate and sanitize all incoming data
Rate Limiting Limit events per user to prevent abuse
Rate Limiting Example
import rateLimit from 'express-rate-limit' ;
const limiter = rateLimit ({
windowMs: 60 * 1000 , // 1 minute
max: 50 , // 50 messages per minute
handler : ( socket ) => {
socket . emit ( 'rate_limit_exceeded' );
}
});
socket . on ( 'send_message' , limiter , handleSendMessage );
Testing Socket.IO
Example test with Socket.IO client:
import { io as Client } from 'socket.io-client' ;
describe ( 'Chat Socket' , () => {
let clientSocket ;
beforeEach (( done ) => {
clientSocket = Client ( 'http://localhost:3002' , {
auth: { token: 'test-token' }
});
clientSocket . on ( 'connect' , done );
});
afterEach (() => {
clientSocket . close ();
});
it ( 'should receive messages' , ( done ) => {
clientSocket . on ( 'new_message' , ( message ) => {
expect ( message . content ). toBe ( 'Hello' );
done ();
});
clientSocket . emit ( 'send_message' , {
conversationId: '123' ,
content: 'Hello'
});
});
});
Next Steps
API Reference Explore REST API endpoints
Widget Integration Add the chat widget to your site
Development Setup Set up your environment
Architecture System architecture overview