Real-Time
Socket.IO architecture with Redis adapter
Vezta uses Socket.IO for real-time communication between the backend and all connected clients. The WebSocket server runs at the /ws namespace and supports 8 channels covering market data, user events, and platform signals.
Server Configuration
The MarketDataGateway is a NestJS WebSocket gateway decorated with @WebSocketGateway:
@WebSocketGateway({ cors: { origin: '*' }, namespace: '/ws' })
export class MarketDataGateway
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
@WebSocketServer()
server: Server;
}Key details:
- Namespace:
/ws - CORS: Open to all origins (authentication is handled via JWT in the handshake)
- Adapter: Redis adapter for horizontal scaling across multiple backend instances
Channels
| Channel | Auth Required | Description |
|---|---|---|
market:prices | No | Real-time YES/NO price updates for active markets |
market:orderbook | No | Order book depth snapshots |
market:trades | No | Live trade feed from exchanges |
user:orders | Yes | Order status changes (fills, cancellations) |
user:notifications | Yes | In-app notification delivery |
user:portfolio | Yes | Portfolio value and position updates |
user:copy-trade | Yes | Copy trade execution events |
monitor:signals | No | News signals and whale activity alerts |
Subscribe / Unsubscribe
Clients join channels by sending a subscribe event with the channel name. The gateway adds the client to a Socket.IO room matching the channel:
// Subscribe
socket.emit('subscribe', { channel: 'market:prices' });
// Server response
// { event: 'subscribed', data: { channel: 'market:prices' } }
// Unsubscribe
socket.emit('unsubscribe', { channel: 'market:prices' });
// Server response
// { event: 'unsubscribed', data: { channel: 'market:prices' } }Under the hood, subscribe calls client.join(data.channel) and unsubscribe calls client.leave(data.channel).
Broadcast Pattern
Backend services broadcast data to channels using the gateway's broadcastToChannel method:
broadcastToChannel(channel: string, event: string, data: any) {
this.server.to(channel).emit('data', { channel, data });
}All broadcasts emit a consistent 'data' event with a { channel, data } payload. The client routes messages by inspecting the channel field:
socket.on('data', (message) => {
switch (message.channel) {
case 'market:prices':
handlePriceUpdate(message.data);
break;
case 'user:orders':
handleOrderUpdate(message.data);
break;
// ...
}
});Authentication
The web client connects with JWT authentication in the Socket.IO handshake:
import { io } from 'socket.io-client';
const socket = io('wss://backend.vezta.io/ws', {
transports: ['websocket'],
auth: {
token: accessToken,
},
});Public channels (market:*, monitor:signals) do not require authentication. User-specific channels (user:*) require a valid JWT.
Client Integration
Web (vezta-fe)
The WsProvider wraps Socket.IO in Jotai atoms for reactive state management:
- Connects when the user is authenticated, disconnects on logout
- WebSocket-only transport with exponential backoff reconnection (1s initial, 30s max)
- Global subscriptions:
user:orders,user:notifications,monitor:signals - Per-market subscriptions via
useMarketSubscription(marketId)hook for prices, orderbook, and trades - Connection status tracked in
wsStatusAtom
Mobile (vezta-mobile)
The mobile WebSocket client is adapted from the web version with additional AppState handling:
- Disconnects when the app goes to background
- Reconnects when the app returns to foreground
- Same Jotai atom pattern for reactive state
Redis Adapter
The Socket.IO server uses the Redis adapter to synchronize events across multiple Node.js processes. When a service broadcasts to a channel, Redis pub/sub ensures the message reaches all connected clients regardless of which process they are connected to.
This is essential for horizontal scaling -- multiple backend instances can run behind a load balancer while maintaining consistent real-time delivery.