websocket-realtime

WebSocket & Realtime Engineer — Institutional capability and runbook for websocket-realtime.

SKILL.md

GitHub

SKILL: WebSocket & Realtime Engineer

Domain: Unified Stream, Pub/Sub, Live Data, Redis Channels

Activation triggers: WebSocket endpoint, live price feed, real-time positions, signal streaming, notification push, pub/sub channel, Redis streams, connection management, reconnection logic.


Unified WebSocket Architecture

Frontend (React)
  └── useExpertWS() hook
        └── WebSocketManager (singleton)
              └── wss://localhost:8002/ws/expert?token=<jwt>

Expert Backend
  └── /ws/expert (FastAPI WebSocket endpoint)
        └── ConnectionManager
              └── Redis Pub/Sub subscriber
                    ├── channel: positions.<account_id>
                    ├── channel: signals.<provider_id>
                    ├── channel: notifications.<user_id>
                    └── channel: market.prices.<symbol>

Backend WebSocket Endpoint

# integral-expert-backend/app/api/v1/ws/expert.py
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends
from app.core.websocket import ConnectionManager
from app.core.auth import get_ws_user   # verify JWT from query param

router = APIRouter()
manager = ConnectionManager()

@router.websocket("/ws/expert")
async def expert_ws_endpoint(
    websocket: WebSocket,
    token: str,
    db: AsyncSession = Depends(get_async_db),
):
    user = await get_ws_user(token, db)
    if not user:
        await websocket.close(code=4001)
        return

    await manager.connect(websocket, user.id)
    try:
        async for message in websocket.iter_json():
            # Handle client → server messages (subscriptions)
            await handle_client_message(message, websocket, user)
    except WebSocketDisconnect:
        pass
    finally:
        await manager.disconnect(websocket, user.id)

ConnectionManager with Redis Pub/Sub

# app/core/websocket.py
import asyncio
import json
from typing import DefaultDict, Set
from collections import defaultdict
from fastapi import WebSocket
import aioredis

class ConnectionManager:
    def __init__(self):
        self._connections: DefaultDict[str, Set[WebSocket]] = defaultdict(set)
        self._redis: aioredis.Redis | None = None
        self._pubsub_task: asyncio.Task | None = None

    async def connect(self, ws: WebSocket, user_id: str) -> None:
        await ws.accept()
        self._connections[user_id].add(ws)
        await self._ensure_pubsub()

    async def disconnect(self, ws: WebSocket, user_id: str) -> None:
        self._connections[user_id].discard(ws)
        if not self._connections[user_id]:
            del self._connections[user_id]

    async def broadcast_to_user(self, user_id: str, payload: dict) -> None:
        dead: Set[WebSocket] = set()
        for ws in self._connections.get(user_id, set()):
            try:
                await ws.send_json(payload)
            except Exception:
                dead.add(ws)
        for ws in dead:
            self._connections[user_id].discard(ws)

    async def publish(self, channel: str, payload: dict) -> None:
        """Publish to Redis — picked up by ALL backend instances."""
        if self._redis:
            await self._redis.publish(channel, json.dumps(payload))

    async def _ensure_pubsub(self) -> None:
        if self._pubsub_task and not self._pubsub_task.done():
            return
        self._redis = await aioredis.from_url(settings.REDIS_URL)
        self._pubsub_task = asyncio.create_task(self._pubsub_listener())

    async def _pubsub_listener(self) -> None:
        pubsub = self._redis.pubsub()
        await pubsub.psubscribe("positions.*", "signals.*", "notifications.*")
        async for message in pubsub.listen():
            if message["type"] == "pmessage":
                channel = message["channel"].decode()
                data = json.loads(message["data"])
                # Route to correct user connections
                user_id = channel.split(".")[-1]
                await self.broadcast_to_user(user_id, {"channel": channel, "data": data})

Frontend WebSocket Manager (Singleton)

// app/src/shared/ws/WebSocketManager.ts
type Handler = (payload: unknown) => void;

class WebSocketManager {
  private ws: WebSocket | null = null;
  private handlers: Map<string, Set<Handler>> = new Map();
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  private reconnectDelay = 1000;
  private maxDelay = 30_000;

  connect(token: string): void {
    if (this.ws?.readyState === WebSocket.OPEN) return;

    const isDev = import.meta.env.DEV;
    const base = isDev ? 'ws://localhost:8002' : (import.meta.env.VITE_EXPERT_WS_URL ?? '');
    this.ws = new WebSocket(`${base}/ws/expert?token=${token}`);

    this.ws.onopen = () => {
      this.reconnectDelay = 1000;   // reset backoff on success
    };

    this.ws.onmessage = (event) => {
      const msg = JSON.parse(event.data) as { channel: string; data: unknown };
      this.handlers.get(msg.channel)?.forEach(h => h(msg.data));
    };

    this.ws.onclose = () => this._scheduleReconnect(token);
    this.ws.onerror = () => this.ws?.close();
  }

  subscribe(channel: string, handler: Handler): () => void {
    if (!this.handlers.has(channel)) this.handlers.set(channel, new Set());
    this.handlers.get(channel)!.add(handler);
    return () => this.handlers.get(channel)?.delete(handler);
  }

  disconnect(): void {
    if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
    this.ws?.close();
    this.ws = null;
  }

  private _scheduleReconnect(token: string): void {
    this.reconnectTimer = setTimeout(() => {
      this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxDelay);
      this.connect(token);
    }, this.reconnectDelay);
  }
}

export const wsManager = new WebSocketManager();

Frontend Hook

// app/src/shared/ws/useExpertWS.ts
import { useEffect } from 'react';
import { wsManager } from './WebSocketManager';
import { useAuthStore } from '@/modules/auth/store/authStore';

export function useExpertWS() {
  const token = useAuthStore(s => s.accessToken);

  useEffect(() => {
    if (token) wsManager.connect(token);
    return () => wsManager.disconnect();
  }, [token]);

  return { subscribe: wsManager.subscribe.bind(wsManager) };
}

Redis Streams (Durable Event Log)

# For durable events (signal history, audit trail) use Redis Streams
# NOT pub/sub (pub/sub is fire-and-forget)

async def publish_signal_event(redis: aioredis.Redis, signal: ProviderSignal) -> None:
    await redis.xadd(
        f"stream:signals:{signal.provider_id}",
        {"data": signal.model_dump_json()},
        maxlen=10_000,    # cap stream length
    )

async def consume_signal_events(redis: aioredis.Redis, provider_id: str, last_id: str = "0") -> list:
    results = await redis.xread(
        {f"stream:signals:{provider_id}": last_id},
        count=100,
        block=5000,   # wait up to 5s for new events
    )
    return results

WebSocket Authentication (JWT from query param)

# JWT in query param — because WS handshake cannot set Authorization header
# app/core/auth.py
async def get_ws_user(token: str, db: AsyncSession) -> User | None:
    try:
        payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
        user_id = payload.get("sub")
        if not user_id:
            return None
        return await get_user_by_id(db, user_id)
    except JWTError:
        return None

Anti-Patterns

✗ Creating a new WebSocket per component (use singleton manager)
✗ Polling REST while a WebSocket is open (wasteful and inconsistent)
✗ No reconnection logic (WebSocket connections drop regularly)
✗ No exponential backoff on reconnect (floods server on outage)
✗ Trusting WS messages without validation (validate schema on receipt)
✗ Storing WebSocket in React state (breaks singleton pattern)
✗ Using pub/sub for audit trail (use Redis Streams — pub/sub is lossy)
✗ Sending JWT as WS message body (use query param on handshake)
✗ Broadcasting to all users (always scope to user_id or account_id)