node-collab

Full-Stack
2026-05-09T11:18:22Z
TypeScriptNode.jsExpressAngularFullstack

node-collab is a multi-room real-time service that demonstrates horizontal scaling done right in Node. Clients open a Socket.IO connection (with a JWT verified in io.use during handshake), join a room, and exchange three event types: chat (reliable), cursor (volatile/lossy), and presence (Redis-backed). The interesting property: the bundled docker-compose stands up TWO server processes plus one Redis. A chat message published from a client connected to instance-a travels through Redis pub/sub via @socket.io/redis-adapter and is delivered to clients connected to instance-b. The headline demo is two browser tabs against two ports, watching messages cross instances.

Cursor updates use socket.volatile.emit so a momentarily-stalled client doesn't accumulate stale cursor positions in the buffer. The lossy semantics match the Go original's TrySend pattern for transient state. Chat messages use the reliable path with ack callbacks. Presence state lives in a Redis hash per room with a 60-second TTL, refreshed periodically, so cross-instance presence works for free.

The slow-client policy is a direct port of collab-board's eviction logic, adapted to Node: a per-socket watcher samples conn.transport.writable on a 250ms tick; if it stays false past the configured threshold (default 2s), the socket is force-disconnected and the eviction is recorded in collab_slow_client_evictions_total{reason}. Like the Go version, this is observable in metrics, not silent. The bench/load.ts harness deliberately spins up 'slow' clients (which never read messages) alongside fast ones, so the eviction counter visibly increments under load.

The frontend is an Angular 20 SPA: standalone components, signals for state, OnPush change detection, no NgModules, no zone-driven CD, no UI framework. Vanilla CSS with a custom 'Observatory Console' design language, dark theme with amber/cyan/magenta/green accent tokens, and an Instrument Serif + IBM Plex Sans/Mono type stack. The app is split into core services (AuthService, CollabService, HealthService, TelemetryService, ThemeService) and feature areas: a home/ lobby (room picker), a room/ view (cursor-canvas, chat-log, chat-composer, presence-list, connection-pill), and an about/ architecture page. The dev server proxies /socket.io, /dev/*, and /healthz to the backend on :3001; the production Docker build is multi-stage — Angular compiles into public/browser/, then Express serves the SPA with fallback alongside the Socket.IO and REST routes from a single port.

The backend stack is intentionally small: socket.io, ioredis, jsonwebtoken, zod, pino, prom-client. Three TODO blocks mark the design decisions worth experimenting with: JWT mid-session expiry policy, slow-client detection lever (transport-stalled vs ack-timeout), and per-room metric cardinality. The whole thing deploys to Railway with replica count = 2 and session affinity, mirroring the local docker-compose layout.

  • Two-replica deployment with Socket.IO Redis adapter: messages cross instances transparently via pub/sub
  • JWT handshake authentication via io.use; tokens minted by a dev-only /dev/token endpoint for the demo
  • Per-room presence state stored in Redis hashes with 60s TTL, refreshed periodically, cross-instance for free
  • Reliable channel for chat (with ack callbacks), volatile channel for cursor updates (lossy by design)
  • Slow-client policy ported from the Go collab-board: transport-stalled watcher + force-disconnect on threshold
  • Prometheus metrics: active_sockets, active_rooms, messages_published, messages_delivered, slow_client_evictions
  • Bench harness (bench/load.ts) deliberately spawns slow clients to exercise and verify the eviction logic
  • Strict TypeScript (ESM, noUncheckedIndexedAccess) with Zod validation on every inbound payload
  • Multi-stage Dockerfile ending in distroless/nodejs22; docker-compose stands up redis + 2 server replicas
  • railway.json pins numReplicas: 2. The horizontal-scaling story is a real public URL, not a slide
  • Angular 20 SPA — standalone components, signals for state, OnPush change detection, no NgModules, no UI framework
  • Custom 'Observatory Console' design language: vanilla CSS, dark theme, IBM Plex + Instrument Serif typography, hand-crafted color tokens
  • Single-process production deploy: multi-stage Dockerfile builds Angular into public/browser/, then Express serves the SPA with fallback alongside the Socket.IO and REST routes from one port