Skip to main content

WebSocket API

Real-time bidirectional communication for live GPS updates and notifications.

Endpoint

ws://localhost:3000/v1/ws
wss://api.elcto.com/v1/ws

Use ws:// for local development and wss:// (secure WebSocket) for production.

Connection

JavaScript

const ws = new WebSocket('wss://api.elcto.com/v1/ws');

ws.onopen = () => {
console.log('WebSocket connected');
};

ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log('Received:', message);
};

ws.onerror = (error) => {
console.error('WebSocket error:', error);
};

ws.onclose = () => {
console.log('WebSocket disconnected');
};

Python

import websocket
import json

def on_message(ws, message):
data = json.loads(message)
print(f"Received: {data}")

def on_open(ws):
print("WebSocket connected")
# Subscribe to GPS channel
ws.send(json.dumps({
"type": "Subscribe",
"channel": "gps"
}))

ws = websocket.WebSocketApp(
"wss://api.elcto.com/v1/ws",
on_open=on_open,
on_message=on_message
)

ws.run_forever()

Message Types

All WebSocket messages are JSON-encoded with a type field.

Client → Server Messages

Subscribe to Channel

Subscribe to receive updates from a specific channel.

{
"type": "Subscribe",
"channel": "gps"
}

Available Channels:

  • user:{userId} - User-specific updates (permissions, roles, account status)
  • admin:* - Admin broadcast channels (requires admin:read permission)
  • public / public:* - Public broadcast channels
  • gps - GPS data updates
  • notifications - System notifications
  • stats - Statistics updates

Unsubscribe from Channel

Stop receiving updates from a channel.

{
"type": "Unsubscribe",
"channel": "gps"
}

Ping

Send a heartbeat ping to keep the connection alive.

{
"type": "Ping"
}

Server → Client Messages

Pong

Response to a ping message.

{
"type": "Pong"
}

Message

Data update from a subscribed channel.

{
"type": "Message",
"channel": "gps",
"data": "{\"id\":\"abc-123\",\"latitude\":51.5074,\"longitude\":-0.1278}"
}

Error

Error response when a request fails.

{
"type": "Error",
"message": "Channel not found"
}

Heartbeat

The server sends automatic ping messages every 5 seconds to keep the connection alive. Clients should respond with pong messages, or the connection will be closed after 10 seconds of inactivity.

Automatic Heartbeat Handling

const ws = new WebSocket('wss://api.elcto.com/v1/ws');

ws.onmessage = (event) => {
const message = JSON.parse(event.data);

// Auto-respond to ping
if (message.type === 'Ping') {
ws.send(JSON.stringify({ type: 'Pong' }));
return;
}

// Handle other messages
handleMessage(message);
};

Channel Subscriptions

GPS Updates

Subscribe to real-time GPS location updates:

ws.onopen = () => {
ws.send(JSON.stringify({
type: 'Subscribe',
channel: 'gps'
}));
};

ws.onmessage = (event) => {
const message = JSON.parse(event.data);

if (message.type === 'Message' && message.channel === 'gps') {
const gpsData = JSON.parse(message.data);
console.log('New GPS location:', gpsData);
// Update map, UI, etc.
}
};

Notifications

Subscribe to system notifications:

ws.send(JSON.stringify({
type: 'Subscribe',
channel: 'notifications'
}));

Multiple Subscriptions

You can subscribe to multiple channels on the same connection:

ws.onopen = () => {
// Subscribe to GPS updates
ws.send(JSON.stringify({
type: 'Subscribe',
channel: 'gps'
}));

// Subscribe to notifications
ws.send(JSON.stringify({
type: 'Subscribe',
channel: 'notifications'
}));
};

Connection Management

Reconnection Strategy

Implement automatic reconnection for production applications:

class WebSocketClient {
constructor(url) {
this.url = url;
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
this.connect();
}

connect() {
this.ws = new WebSocket(this.url);

this.ws.onopen = () => {
console.log('Connected');
this.reconnectDelay = 1000;
this.resubscribe();
};

this.ws.onclose = () => {
console.log('Disconnected, reconnecting...');
setTimeout(() => {
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
);
this.connect();
}, this.reconnectDelay);
};

this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};

this.ws.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data));
};
}

resubscribe() {
// Resubscribe to channels after reconnection
this.subscriptions.forEach(channel => {
this.subscribe(channel);
});
}

subscribe(channel) {
this.subscriptions.add(channel);
this.ws.send(JSON.stringify({
type: 'Subscribe',
channel
}));
}

handleMessage(message) {
if (message.type === 'Ping') {
this.ws.send(JSON.stringify({ type: 'Pong' }));
return;
}

// Handle other message types
console.log('Received:', message);
}
}

// Usage
const client = new WebSocketClient('wss://api.elcto.com/v1/ws');
client.subscribe('gps');

Complete Example

class GpsTracker {
constructor() {
this.ws = null;
this.connected = false;
this.subscribers = new Set();
this.connect();
}

connect() {
this.ws = new WebSocket('wss://api.elcto.com/v1/ws');

this.ws.onopen = () => {
console.log('✅ WebSocket connected');
this.connected = true;

// Subscribe to GPS updates
this.send({
type: 'Subscribe',
channel: 'gps'
});
};

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 disconnected');
this.connected = false;

// Attempt reconnection after 3 seconds
setTimeout(() => this.connect(), 3000);
};
}

handleMessage(message) {
switch (message.type) {
case 'Ping':
this.send({ type: 'Pong' });
break;

case 'Message':
if (message.channel === 'gps') {
const gpsData = JSON.parse(message.data);
this.notifySubscribers(gpsData);
}
break;

case 'Error':
console.error('Server error:', message.message);
break;
}
}

send(data) {
if (this.connected && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}

subscribe(callback) {
this.subscribers.add(callback);
}

unsubscribe(callback) {
this.subscribers.delete(callback);
}

notifySubscribers(data) {
this.subscribers.forEach(callback => callback(data));
}
}

// Usage
const tracker = new GpsTracker();

tracker.subscribe((gpsData) => {
console.log('📍 New location:', gpsData.latitude, gpsData.longitude);
// Update your map, UI, etc.
});

Authentication

WebSocket connections require authentication via a session token. The token can be passed as a query parameter or via cookie.

Query Parameter Authentication

const sessionToken = 'sess_abc123...'; // From NextAuth session
const ws = new WebSocket(`wss://api.elcto.com/v1/ws?token=${sessionToken}`);

Using the Heimdall API Client

import { createWebSocket } from '@/lib/api/websocket';

const ws = createWebSocket('/v1/ws', {
accessToken: session.accessToken, // From NextAuth session
autoReconnect: true,
reconnectDelay: 5000,
maxReconnectAttempts: 10,
});

ws.onMessage((message) => {
console.log('Received:', message);
});

Authentication Flow

  1. User logs in via NextAuth (credentials or OAuth)
  2. NextAuth creates a database session via POST /v1/sessions
  3. Session token is stored in session.accessToken
  4. WebSocket connection uses this token for authentication
  5. On logout, session is deleted via DELETE /v1/sessions/{token}

Channel Access Control (RBAC)

Channel subscriptions are controlled by role-based permissions:

Channel TypeAccess Rule
user:{userId}Only the user can subscribe to their own channel
admin:*Requires admin:read permission or super_admin role
public / public:*Anyone authenticated can subscribe

Example: User Channel

// Auto-subscribed when authenticated - receives permission/role updates
ws.onmessage = (event) => {
const message = JSON.parse(event.data);

if (message.type === 'permissionsUpdated') {
// User's permissions changed
updateLocalPermissions(message.permissions);
}

if (message.type === 'rolesUpdated') {
// User's roles changed
updateLocalRoles(message.roles);
}
};

User Status Events

The WebSocket automatically delivers user status events for account management:

Account Deleted

Sent when a user's account is deleted (scheduled deletion completed).

{
"type": "accountDeleted",
"userId": "user-123",
"reason": "Account deleted",
"timestamp": "2025-01-25T10:00:00Z"
}

Account Banned

Sent when a user is banned by an administrator.

{
"type": "accountBanned",
"userId": "user-123",
"reason": "Terms of service violation",
"expiresAt": "2025-02-25T10:00:00Z",
"isPermanent": false,
"timestamp": "2025-01-25T10:00:00Z"
}

Session Revoked

Sent when a specific session is revoked.

{
"type": "sessionRevoked",
"userId": "user-123",
"sessionId": "sess-456",
"reason": "Password changed",
"timestamp": "2025-01-25T10:00:00Z"
}

Force Logout

Sent when all sessions should be terminated (e.g., security concern).

{
"type": "forceLogout",
"userId": "user-123",
"reason": "Security concern",
"timestamp": "2025-01-25T10:00:00Z"
}

Handling User Status Events

ws.onMessage((message) => {
// Handle user account status events
if (
message.type === 'accountDeleted' ||
message.type === 'accountBanned' ||
message.type === 'sessionRevoked' ||
message.type === 'forceLogout'
) {
// Close WebSocket and sign out
ws.close();

const errorMap = {
accountDeleted: 'AccountDeleted',
accountBanned: 'AccountBanned',
sessionRevoked: 'SessionRevoked',
forceLogout: 'ForceLogout',
};

signOut({ callbackUrl: `/login?error=${errorMap[message.type]}` });
return;
}

// Handle other messages...
});

Permission & Role Updates

Real-time permission and role changes are broadcast to affected users:

Permissions Updated

{
"type": "permissionsUpdated",
"userId": "user-123",
"permissions": ["users:read", "gps:read", "gps:write"]
}

Roles Updated

{
"type": "rolesUpdated",
"userId": "user-123",
"roles": ["viewer", "gps_manager"]
}

Role Permissions Changed

Sent when a role's permissions are modified (triggers refresh):

{
"type": "rolePermissionsChanged",
"roleId": "role-123",
"roleName": "viewer"
}

Rate Limiting

WebSocket connections are not rate-limited, but excessive message sending may result in connection closure.

Best Practices

WebSocket Best Practices
  1. Implement reconnection - Handle disconnections gracefully with exponential backoff
  2. Respond to pings - Always respond to server ping messages to keep connection alive
  3. Track subscriptions - Remember subscribed channels for reconnection
  4. Handle errors - Implement proper error handling for all message types
  5. Clean up - Close connections when no longer needed
  6. Use compression - Enable WebSocket compression for bandwidth efficiency

Monitoring

Track WebSocket connection status via the health endpoint:

curl https://api.elcto.com/health

Response:

{
"status": "healthy",
"services": {
"websocket": {
"status": "up",
"connections": 42,
"subscriptions": 128
}
}
}

Next Steps