Real-Time
Socket.IO architecture, channels, and Redis adapter
Vezta uses Socket.IO for real-time communication between the backend and connected clients. The WebSocket server runs at the /ws namespace and exposes channels covering market data, user events, crypto prices, sports state, and platform signals. Public channel prefixes (no auth): market:, monitor:, crypto:prices, sports:, weather:.
Server Configuration
MarketDataGateway is a NestJS WebSocket gateway:
@WebSocketGateway({ cors: { origin: '*' }, namespace: '/ws' })
export class MarketDataGateway
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
@WebSocketServer()
server: Server;
}Key details:
- Namespace:
/ws - CORS: Open at the gateway level; the actual CORS gate is enforced at Nginx (whitelist for
vezta.io,dev.vezta.io,admin.vezta.io,localhost:3000) - Adapter: Redis adapter for horizontal scaling across worker processes
Channels
| Channel | Auth Required | Description |
|---|---|---|
market:prices:<id> | No | Real-time YES/NO price updates for active markets |
market:orderbook:<id> | No | Order book depth snapshots |
market:trades:<id> | No | Live trade feed from exchanges |
market:holders:<id> | No | Top-holders concentration (Polymarket only) |
market:targetprices:<id> | No | Target-price / TP-SL threshold updates |
market:sports-state:<id> | No | Live sports market state, keyed by market id |
market:sports-stats:<eventId> | No | Live sports-stats deltas, keyed by Event id |
sports:standings:<leagueSlug> | No | League standings refresh (e.g. fifa.world) |
crypto:prices | No | Chainlink oracle prices for BTC/ETH/SOL/XRP via Polymarket RTDS |
weather:<icao> | No | Weather-station temperature readings for weather markets |
user:orders | Yes | Order status changes (fills, cancellations) |
user:notifications | Yes | In-app notification delivery |
user:transactions | Yes | Deposit / withdrawal status updates |
user:portfolio | Yes | Portfolio value and position updates |
user:copy-trade | Yes | Copy trade execution events |
user:combo-orders | Yes | Combo (multi-leg) order status updates |
monitor:signals | No | News signals + insider alerts (event insider-alert for the latter) |
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).
Connection and Message Limits
The gateway enforces per-process limits (in-memory tracking, cleared on disconnect):
- 5000 max total connections
- 10 connections per
userId - 20 connections per IP
- 30 messages/min per client (subscribe + unsubscribe combined)
When a limit is hit, the gateway drops the offending socket.
Broadcast Pattern
Backend services broadcast data to channels using gateway.broadcastToChannel():
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 message.channel.
For insider alerts, the backend uses gateway.broadcastToChannel('monitor:signals', 'insider-alert', ...) -- not raw server.emit, which would bypass rooms and the wrapper.
Authentication
The web client connects with JWT 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:*, crypto:prices, monitor:signals) do not require authentication. User-specific channels (user:*) require a valid JWT and ownership verification at subscribe time.
Client Integration
Web (vezta-fe)
WsProvider (lib/ws/provider.tsx) wraps Socket.IO in Jotai atoms:
- Connects when authenticated, disconnects on logout
- WebSocket-only transport, exponential backoff reconnect (1s → 30s)
- Global subscriptions:
user:orders,user:notifications,monitor:signals - Per-market hook:
useMarketSubscription(marketId)for prices, orderbook, trades - Connection status tracked in
wsStatusAtom
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 the worker split, where the API container plus specialized workers (worker-traders, worker-market, worker-misc, worker-websocket, worker-rtds, worker-ai, worker-sports, worker-combos) all share the same Redis. The API container holds the WebSocket connections; workers publish to channels via Redis pub/sub; clients receive updates regardless of which worker produced them.