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.
ws://localhost:8888/wswss://localhost:8443/ws or wss://yourdomain.com/ws✅ 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
const token = 'your_jwt_token';
const ws = new WebSocket(`ws://localhost:8888/ws?token=${token}`);
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8888/ws', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const ws = new WebSocket('ws://localhost:8888/ws', [`token-${token}`]);
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
All messages are JSON-formatted strings.
{
"type": "subscribe",
"channel": "apps:created"
}
{
"type": "unsubscribe",
"channel": "apps:created"
}
{
"type": "ping"
}
{
"type": "list_channels"
}
{
"type": "stats"
}
{
"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"
}
{
"type": "subscribed",
"channel": "apps:created",
"timestamp": "2025-11-25T12:00:00.000Z"
}
{
"type": "event",
"channel": "apps:created",
"data": {
"id": "app-uuid",
"name": "my-app",
"status": "pending"
},
"timestamp": "2025-11-25T12:00:00.000Z"
}
{
"type": "pong",
"timestamp": "2025-11-25T12:00:00.000Z"
}
{
"type": "error",
"message": "Not authorized to subscribe to this channel",
"timestamp": "2025-11-25T12:00:00.000Z"
}
apps:created - New application createdapps:updated - Application updatedapps:deleted - Application deletedapps:deployed - Application deployedapps:status - Application status changedusers:created - New user registeredusers:updated - User profile updatedusers:deleted - User deleteduser:{userId} - Private channel for specific user (only accessible by the user or admin)database:table_created - New table createddatabase:table_updated - Table schema updateddatabase:table_deleted - Table deleteddatabase:row_updated - Row data changeddeployments:started - Deployment starteddeployments:completed - Deployment finished successfullydeployments:failed - Deployment faileddeployments:status - Deployment status updatestorage:uploaded - File uploadedstorage:deleted - File deletedstorage:updated - File updatedsystem:health - System health updatessystem:metrics - Performance metricssystem:alerts - System alertsannouncements - Public announcementsChannels 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 |
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');
};
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>
);
}
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()
})
);
# 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"}
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'
}));
};
docker ps | grep realtimedocker logs saas-gatewayuser:{userId} must match your userIdcurl http://localhost:8888/health
Response:
{
"status": "ok",
"service": "realtime",
"connections": 5,
"authenticated": 3,
"activeChannels": 8,
"timestamp": "2025-11-25T12:00:00.000Z"
}
{
"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"
}
# 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
# .env file
NODE_ENV=production
JWT_SECRET=your_production_secret_here
REDIS_URL=redis://your-redis-host:6379
The https.conf Nginx configuration automatically redirects HTTP to HTTPS in production.
const WS_URL = process.env.NODE_ENV === 'production'
? 'wss://yourdomain.com/ws'
: 'ws://localhost:8888/ws';
For more information, see the Architecture Documentation