# Technical Design: WebSocket Chat ## Architecture Overview This feature integrates the existing `pkg/realtime` package into the `chat-api` service to provide WebSocket chat functionality with Redis pub/sub for horizontal scaling. ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Client 1 │ │ Client 2 │ │ Client N │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ WebSocket │ WebSocket │ WebSocket ▼ ▼ ▼ ┌──────────────────────────────────────────────────────┐ │ chat-api Pod 1 │ │ ┌─────────────────────────────────────────────────┐ │ │ │ realtime.Handler │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ │ │ LocalHub │ │ │ │ │ │ - connections map[id]Connection │ │ │ │ │ │ - rooms map[room]map[id]struct{} │ │ │ │ │ └─────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ RedisBroadcaster │ │ │ │ - Publish() → Redis channel │ │ │ │ - Run() ← Redis PSubscribe │ │ │ └─────────────────────────────────────────────────┘ │ └──────────────────────────┬───────────────────────────┘ │ ┌────────────┴────────────┐ │ Redis Pub/Sub │ │ realtime:global │ │ realtime:room:{name} │ └────────────┬────────────┘ │ ┌──────────────────────────┴───────────────────────────┐ │ chat-api Pod 2 │ │ (same structure) │ └──────────────────────────────────────────────────────┘ ``` ## Component Design ### 1. Configuration Extension Extend `internal/config/config.go` to include Redis configuration: ```go type Config struct { // ... existing fields ... // Redis RedisURL string } func Load() *Config { return &Config{ // ... existing ... RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"), } } ``` ### 2. Service Bootstrap (main.go) Wire up the realtime components in the service entry point: ```go func main() { cfg := config.Load() ctx := context.Background() // Create hub (local connection registry) hub := realtime.NewHub(logger) go hub.Run(ctx) // Create Redis broadcaster (for multi-pod) var broadcaster realtime.Broadcaster if cfg.RedisURL != "" { redisClient := redis.NewClient(parseRedisURL(cfg.RedisURL)) broadcaster = realtime.NewRedisBroadcaster(redisClient, hub, logger) go broadcaster.Run(ctx) } // Register routes with realtime handler api.RegisterRoutes(application, exampleService, hub, broadcaster) } ``` ### 3. Route Registration Add WebSocket routes in `internal/api/routes.go`: ```go func RegisterRoutes( application *app.App, exampleService *service.ExampleService, hub realtime.Hub, broadcaster realtime.Broadcaster, ) { // ... existing routes ... // WebSocket handler wsHandler := realtime.NewHandler(hub, logger, realtime.HandlerConfig{ Broadcaster: broadcaster, OnConnect: func(conn realtime.Connection) { logger.Info("client connected", "id", conn.ID()) }, OnDisconnect: func(conn realtime.Connection) { logger.Info("client disconnected", "id", conn.ID()) }, OnMessage: func(conn realtime.Connection, msg *realtime.Message) *realtime.Message { // Set message type if not specified if msg.Type == "" { msg.Type = realtime.MessageTypeChat } return msg }, AuthRequired: cfg.AuthEnabled, }) // Mount WebSocket routes application.Mount("/api/chat-api/ws", wsHandler.Routes()) } ``` ### 4. Message Flow 1. **Client → Server (Incoming)** - Client sends JSON message via WebSocket - `WSClient.readPump()` decodes message - `OnMessage` callback transforms/filters - `RedisBroadcaster.Publish()` sends to Redis channel 2. **Redis → All Pods (Distribution)** - Redis delivers to all subscribed pods - `RedisBroadcaster.Run()` receives via PSubscribe - Filters out messages from same pod (echo prevention) - Forwards to `LocalHub.Broadcast()` 3. **Server → Clients (Outgoing)** - `LocalHub.doBroadcast()` iterates room connections - `WSClient.Send()` queues message - `WSClient.writePump()` writes to WebSocket ### 5. Redis Channel Naming | Pattern | Usage | |---------|-------| | `realtime:global` | Messages without room (broadcast to all) | | `realtime:room:{name}` | Room-specific messages | ### 6. Graceful Shutdown The existing `pkg/realtime` package handles: - Context cancellation triggers hub shutdown - Hub closes all connections - WSClient sends close frame - Redis broadcaster closes subscription ## File Changes | File | Change | |------|--------| | `services/chat-api/internal/config/config.go` | Add `RedisURL` field | | `services/chat-api/cmd/server/main.go` | Wire hub, broadcaster, Redis client | | `services/chat-api/internal/api/routes.go` | Mount WebSocket handler | | `services/chat-api/internal/api/spec.go` | Add WebSocket endpoint documentation | ## Testing Strategy ### Unit Tests - Config loading with Redis URL - Message transformation in OnMessage callback ### Integration Tests - WebSocket connection upgrade - Message send/receive flow - Room-based isolation - Multi-client broadcast ### Manual Testing ```bash # Terminal 1: Start server ./scripts/dev.sh # Terminal 2: Connect with wscat wscat -c ws://localhost:8001/api/chat-api/ws # Terminal 3: Another client wscat -c ws://localhost:8001/api/chat-api/ws # Send message from Terminal 2, verify received in Terminal 3 {"type":"chat","data":{"content":"Hello!"}} ``` ## Security Considerations 1. **Authentication**: Optional auth via `AUTH_ENABLED` config 2. **Origin Check**: WebSocket upgrader validates Origin header (configurable) 3. **Message Size**: 64KB limit prevents memory exhaustion 4. **Buffer Limits**: 256-message buffer drops excess (prevents slow-client backpressure) ## Rollout Plan 1. Deploy with `REDIS_URL` set to enable cross-pod messaging 2. Without `REDIS_URL`, operates in single-pod mode (local broadcast only) 3. No database migrations required 4. Feature is additive (no breaking changes)