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 (requiresadmin:readpermission)public/public:*- Public broadcast channelsgps- GPS data updatesnotifications- System notificationsstats- 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
- User logs in via NextAuth (credentials or OAuth)
- NextAuth creates a database session via
POST /v1/sessions - Session token is stored in
session.accessToken - WebSocket connection uses this token for authentication
- On logout, session is deleted via
DELETE /v1/sessions/{token}
Channel Access Control (RBAC)
Channel subscriptions are controlled by role-based permissions:
| Channel Type | Access 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
- Implement reconnection - Handle disconnections gracefully with exponential backoff
- Respond to pings - Always respond to server ping messages to keep connection alive
- Track subscriptions - Remember subscribed channels for reconnection
- Handle errors - Implement proper error handling for all message types
- Clean up - Close connections when no longer needed
- 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
}
}
}