Architecture: How should native WebSocket (/ws) and Socket.IO (/command-ce...

Documentation for Sessions

🏗️ Architecture: How should native WebSocket (/ws) and Socket.IO (/command-ce...

Date: 2026-02-06
Time: 21:02:58 UTC
Session: editor2-97.8
Agent: backend
Confidence: 100%
Status: ✅ Documented


📋 Decision Context

Question:

How should native WebSocket (/ws) and Socket.IO (/command-center, /mcp-stream) coexist on the same HTTP server port?

Task: WebSocket upgrade routing architecture for backend-ai-api


🎯 Decision Made

Chosen:

noServer:true + prependListener to intercept /ws before engine.io

Options Considered:

  1. Separate HTTP servers on different ports
  2. noServer:true + prependListener to intercept /ws before engine.io
  3. Custom IoAdapter with server caching
  4. Use server.on('upgrade') with explicit path routing

💭 Reasoning

NestJS IoAdapter creates a SEPARATE Socket.IO/engine.io instance per @WebSocketGateway (no server caching in createIOServer). Each engine.io attach() adds its own 'upgrade' listener. With destroyUpgrade:true (default), engine.io schedules socket.destroy() after 1s for upgrade requests it doesn't recognize. The solution: (1) NativeWsGateway uses new WebSocketServer({ noServer: true }) so ws doesn't add its own catch-all upgrade handler. (2) Uses server.prependListener('upgrade', ...) so the /ws handler fires BEFORE engine.io handlers. (3) For /ws requests, calls wss.handleUpgrade() which consumes the socket. (4) Setting destroyUpgrade:false on Socket.IO gateways provides belt-and-suspenders safety — engine.io won't destroy sockets claimed by native WS. (5) attachToServer() is called from main.ts AFTER app.listen() to guarantee all Socket.IO gateways are fully wired first.


📁 Files Involved


🔗 Related


Auto-generated by MCP Smart Consolidation