# WebSocket (WS/WSS) Implementation Guide ## Overview The SaaS Platform includes a production-ready WebSocket service for real-time bidirectional communication between clients and the server. The service supports both **WS** (unencrypted) and **WSS** (encrypted over TLS) protocols. ## Connection URLs - **Development (HTTP/WS)**: `ws://localhost:8888/ws` - **Production (HTTPS/WSS)**: `wss://localhost:8443/ws` or `wss://yourdomain.com/ws` ## Features ✅ **JWT Authentication** - Secure connection with JWT tokens ✅ **Channel-based Pub/Sub** - Subscribe to specific event channels ✅ **Authorization** - Role-based and user-specific channel access ✅ **Heartbeat/Ping-Pong** - Automatic connection health monitoring ✅ **Redis Integration** - Scalable pub/sub messaging ✅ **Connection Management** - Track and manage all active connections ✅ **Graceful Reconnection** - Client reconnection support ✅ **Real-time Events** - Live updates for apps, users, database, deployments --- ## Authentication ### Method 1: Query Parameter (Recommended) ```javascript const token = 'your_jwt_token'; const ws = new WebSocket(`ws://localhost:8888/ws?token=${token}`); ``` ### Method 2: Authorization Header (Node.js/Advanced) ```javascript const WebSocket = require('ws'); const ws = new WebSocket('ws://localhost:8888/ws', { headers: { 'Authorization': `Bearer ${token}` } }); ``` ### Method 3: Sec-WebSocket-Protocol (Browser Compatibility) ```javascript const ws = new WebSocket('ws://localhost:8888/ws', [`token-${token}`]); ``` --- ## Connection Flow ``` 1. Client connects to ws://localhost:8888/ws?token=JWT 2. Server extracts and verifies JWT token 3. Server sends "connected" message with user info and authorized channels 4. Client can now subscribe to channels and receive events ``` --- ## Message Protocol All messages are JSON-formatted strings. ### Client → Server Messages #### Subscribe to Channel ```json { "type": "subscribe", "channel": "apps:created" } ``` #### Unsubscribe from Channel ```json { "type": "unsubscribe", "channel": "apps:created" } ``` #### Ping (Keep-Alive) ```json { "type": "ping" } ``` #### List Available Channels ```json { "type": "list_channels" } ``` #### Get Connection Stats (Admin Only) ```json { "type": "stats" } ``` ### Server → Client Messages #### Connected (Welcome Message) ```json { "type": "connected", "connectionId": "uuid-v4", "user": { "userId": "user-uuid", "email": "user@example.com", "role": "admin" }, "authorizedChannels": [ "apps:created", "apps:updated", "user:user-uuid", "system:health" ], "timestamp": "2025-11-25T12:00:00.000Z" } ``` #### Subscription Confirmed ```json { "type": "subscribed", "channel": "apps:created", "timestamp": "2025-11-25T12:00:00.000Z" } ``` #### Event Received ```json { "type": "event", "channel": "apps:created", "data": { "id": "app-uuid", "name": "my-app", "status": "pending" }, "timestamp": "2025-11-25T12:00:00.000Z" } ``` #### Pong (Heartbeat Response) ```json { "type": "pong", "timestamp": "2025-11-25T12:00:00.000Z" } ``` #### Error ```json { "type": "error", "message": "Not authorized to subscribe to this channel", "timestamp": "2025-11-25T12:00:00.000Z" } ``` --- ## Available Channels ### Application Events - `apps:created` - New application created - `apps:updated` - Application updated - `apps:deleted` - Application deleted - `apps:deployed` - Application deployed - `apps:status` - Application status changed ### User Events - `users:created` - New user registered - `users:updated` - User profile updated - `users:deleted` - User deleted ### User-Specific Channel (Private) - `user:{userId}` - Private channel for specific user (only accessible by the user or admin) ### Database Events - `database:table_created` - New table created - `database:table_updated` - Table schema updated - `database:table_deleted` - Table deleted - `database:row_updated` - Row data changed ### Deployment Events - `deployments:started` - Deployment started - `deployments:completed` - Deployment finished successfully - `deployments:failed` - Deployment failed - `deployments:status` - Deployment status update ### Storage Events - `storage:uploaded` - File uploaded - `storage:deleted` - File deleted - `storage:updated` - File updated ### System Events (Admin Only) - `system:health` - System health updates - `system:metrics` - Performance metrics - `system:alerts` - System alerts ### Public Channels (No Auth Required) - `announcements` - Public announcements --- ## Channel Authorization Channels have different authorization requirements: | Channel Pattern | Requires Auth | Allowed Roles | Special Rules | |----------------|---------------|---------------|---------------| | `announcements` | No | All | Public channel | | `apps:*` | Yes | All authenticated | - | | `users:*` | Yes | All authenticated | - | | `user:{userId}` | Yes | All authenticated | Only own user or admin | | `database:*` | Yes | All authenticated | - | | `deployments:*` | Yes | All authenticated | - | | `storage:*` | Yes | All authenticated | - | | `system:*` | Yes | Admin only | Restricted to admins | --- ## JavaScript Client Example ### Basic Connection ```javascript class RealtimeClient { constructor(token, url = 'ws://localhost:8888/ws') { this.token = token; this.url = url; this.ws = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 1000; } connect() { // Connect with token as query parameter this.ws = new WebSocket(`${this.url}?token=${this.token}`); this.ws.onopen = () => { console.log('WebSocket connected'); this.reconnectAttempts = 0; }; this.ws.onmessage = (event) => { const message = JSON.parse(event.data); this.handleMessage(message); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); }; this.ws.onclose = () => { console.log('WebSocket closed'); this.reconnect(); }; } handleMessage(message) { console.log('Received:', message); switch (message.type) { case 'connected': console.log('Connection ID:', message.connectionId); console.log('Authorized channels:', message.authorizedChannels); break; case 'subscribed': console.log(`Subscribed to ${message.channel}`); break; case 'event': console.log(`Event on ${message.channel}:`, message.data); // Handle your event here break; case 'error': console.error('Server error:', message.message); break; case 'pong': console.log('Pong received'); break; } } subscribe(channel) { this.send({ type: 'subscribe', channel: channel }); } unsubscribe(channel) { this.send({ type: 'unsubscribe', channel: channel }); } send(data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(data)); } else { console.error('WebSocket is not open'); } } reconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`); setTimeout(() => { this.connect(); }, delay); } else { console.error('Max reconnection attempts reached'); } } disconnect() { if (this.ws) { this.ws.close(); } } } // Usage const token = 'your_jwt_token_here'; const client = new RealtimeClient(token, 'ws://localhost:8888/ws'); client.connect(); // Subscribe to channels client.ws.onopen = () => { client.subscribe('apps:created'); client.subscribe('users:updated'); client.subscribe('user:my-user-id'); }; ``` ### React Hook Example ```javascript import { useEffect, useRef, useState } from 'react'; export function useWebSocket(token, channels = []) { const [connected, setConnected] = useState(false); const [messages, setMessages] = useState([]); const wsRef = useRef(null); useEffect(() => { if (!token) return; const ws = new WebSocket(`ws://localhost:8888/ws?token=${token}`); wsRef.current = ws; ws.onopen = () => { console.log('WebSocket connected'); setConnected(true); // Subscribe to channels channels.forEach(channel => { ws.send(JSON.stringify({ type: 'subscribe', channel })); }); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); if (message.type === 'event') { setMessages(prev => [...prev, message]); } }; ws.onclose = () => { console.log('WebSocket disconnected'); setConnected(false); }; ws.onerror = (error) => { console.error('WebSocket error:', error); }; return () => { ws.close(); }; }, [token, channels]); const send = (data) => { if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify(data)); } }; return { connected, messages, send }; } // Usage in component function MyComponent() { const { connected, messages, send } = useWebSocket( localStorage.getItem('token'), ['apps:created', 'users:updated'] ); return (

Status: {connected ? 'Connected' : 'Disconnected'}

); } ``` --- ## Publishing Events from Backend To publish events to WebSocket clients from your backend services: ```typescript import { createClient } from 'redis'; const redisPublisher = createClient({ url: process.env.REDIS_URL }); await redisPublisher.connect(); // Publish an event await redisPublisher.publish( 'apps:created', JSON.stringify({ id: 'app-uuid', name: 'my-new-app', status: 'pending', createdAt: new Date().toISOString() }) ); ``` --- ## Testing WebSocket Connection ### Using websocat (CLI Tool) ```bash # Install websocat brew install websocat # macOS # or cargo install websocat # Rust # Connect websocat "ws://localhost:8888/ws?token=YOUR_JWT_TOKEN" # Send messages {"type":"subscribe","channel":"apps:created"} {"type":"ping"} ``` ### Using Browser Console ```javascript const token = 'your_jwt_token'; const ws = new WebSocket(`ws://localhost:8888/ws?token=${token}`); ws.onmessage = (event) => { console.log('Received:', JSON.parse(event.data)); }; ws.onopen = () => { // Subscribe to a channel ws.send(JSON.stringify({ type: 'subscribe', channel: 'apps:created' })); }; ``` --- ## Troubleshooting ### Connection Refused - **Check if the realtime service is running**: `docker ps | grep realtime` - **Check Nginx logs**: `docker logs saas-gateway` - **Verify port forwarding**: Port 8888 (HTTP/WS) or 8443 (HTTPS/WSS) ### Authentication Failed - **Check JWT token validity**: Token must not be expired - **Verify JWT_SECRET**: Must match between auth and realtime services - **Check token format**: Should be passed as query parameter or header ### Subscription Denied - **Check user role**: Some channels require admin role - **Verify channel name**: Must match authorized channel patterns - **Check user-specific channels**: `user:{userId}` must match your userId ### Connection Drops - **Check heartbeat**: Server sends ping every 30 seconds - **Network issues**: Check firewall and proxy settings - **Timeout settings**: Nginx WebSocket timeout is set to 24 hours --- ## Security Considerations 1. **Always use WSS in production** - Encrypt WebSocket traffic over TLS 2. **Validate JWT tokens** - Tokens are verified on connection 3. **Channel authorization** - Users can only subscribe to authorized channels 4. **Rate limiting** - Consider implementing rate limits for message sending 5. **Input validation** - All messages are validated before processing --- ## Performance - **Heartbeat interval**: 30 seconds (configurable) - **Heartbeat timeout**: 35 seconds - **Max connections**: Limited by system resources - **Redis pub/sub**: Efficient channel-based broadcasting - **Connection pooling**: Nginx keepalive for upstream connections --- ## Monitoring ### Health Check ```bash curl http://localhost:8888/health ``` Response: ```json { "status": "ok", "service": "realtime", "connections": 5, "authenticated": 3, "activeChannels": 8, "timestamp": "2025-11-25T12:00:00.000Z" } ``` ### Connection Stats (Admin Only via WebSocket) ```json { "type": "stats" } ``` Response: ```json { "type": "stats", "data": { "totalConnections": 10, "authenticatedConnections": 8, "anonymousConnections": 2, "totalSubscriptions": 45, "connectionsByChannel": { "apps:created": 5, "users:updated": 3, "system:health": 1 } }, "timestamp": "2025-11-25T12:00:00.000Z" } ``` --- ## Production Deployment ### 1. Generate Production SSL Certificates ```bash # Use Let's Encrypt for production sudo certbot certonly --standalone -d yourdomain.com -d www.yourdomain.com # Copy certificates to ssl directory cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem ./ssl/certificate.crt cp /etc/letsencrypt/live/yourdomain.com/privkey.pem ./ssl/private.key ``` ### 2. Update Environment Variables ```bash # .env file NODE_ENV=production JWT_SECRET=your_production_secret_here REDIS_URL=redis://your-redis-host:6379 ``` ### 3. Enable HTTPS Redirect The `https.conf` Nginx configuration automatically redirects HTTP to HTTPS in production. ### 4. Update WebSocket URL in Frontend ```javascript const WS_URL = process.env.NODE_ENV === 'production' ? 'wss://yourdomain.com/ws' : 'ws://localhost:8888/ws'; ``` --- ## Next Steps - [ ] Implement custom event handlers in your application - [ ] Set up monitoring and alerting for WebSocket connections - [ ] Configure auto-scaling for the realtime service - [ ] Implement message rate limiting - [ ] Add custom channels for your application's specific needs --- *For more information, see the [Architecture Documentation](./ARCHITECTURE.md)*