sp3-verify-1770325830/.sdlc/features/websocket-chat/design.md
rdev-worker 42c1444274
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature websocket-chat --requirements 'GET /ws upgrades to...
2026-02-05 21:50:17 +00:00

8.0 KiB

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:

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:

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:

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

# 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)