WEBSOCKET.md 14 KB

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)

const token = 'your_jwt_token';
const ws = new WebSocket(`ws://localhost:8888/ws?token=${token}`);

Method 2: Authorization Header (Node.js/Advanced)

const WebSocket = require('ws');

const ws = new WebSocket('ws://localhost:8888/ws', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

Method 3: Sec-WebSocket-Protocol (Browser Compatibility)

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

{
  "type": "subscribe",
  "channel": "apps:created"
}

Unsubscribe from Channel

{
  "type": "unsubscribe",
  "channel": "apps:created"
}

Ping (Keep-Alive)

{
  "type": "ping"
}

List Available Channels

{
  "type": "list_channels"
}

Get Connection Stats (Admin Only)

{
  "type": "stats"
}

Server → Client Messages

Connected (Welcome Message)

{
  "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

{
  "type": "subscribed",
  "channel": "apps:created",
  "timestamp": "2025-11-25T12:00:00.000Z"
}

Event Received

{
  "type": "event",
  "channel": "apps:created",
  "data": {
    "id": "app-uuid",
    "name": "my-app",
    "status": "pending"
  },
  "timestamp": "2025-11-25T12:00:00.000Z"
}

Pong (Heartbeat Response)

{
  "type": "pong",
  "timestamp": "2025-11-25T12:00:00.000Z"
}

Error

{
  "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

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

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 (
    <div>
      <p>Status: {connected ? 'Connected' : 'Disconnected'}</p>
      <ul>
        {messages.map((msg, i) => (
          <li key={i}>
            {msg.channel}: {JSON.stringify(msg.data)}
          </li>
        ))}
      </ul>
    </div>
  );
}

Publishing Events from Backend

To publish events to WebSocket clients from your backend services:

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)

# 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

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

curl http://localhost:8888/health

Response:

{
  "status": "ok",
  "service": "realtime",
  "connections": 5,
  "authenticated": 3,
  "activeChannels": 8,
  "timestamp": "2025-11-25T12:00:00.000Z"
}

Connection Stats (Admin Only via WebSocket)

{
  "type": "stats"
}

Response:

{
  "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

# 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

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

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