WebSocket Client
The WebSocket client provides real-time communication with the Heimdall API. It's designed for client-side use only.
createWebSocket
Create a WebSocket connection with auto-reconnect support. The first argument is the
ApiClientConfig — the connection URL is derived from it
(config.wsUrl, falling back to config.baseUrl with http→ws and /v1/ws). There
is no longer a separate endpoint argument.
import { getApiConfig, createWebSocket } from "@/lib/api";
const ws = createWebSocket(getApiConfig(), {
accessToken: session.accessToken,
});
The call site is unchanged, but internally the client now uses the Ticket-Era handshake.
When an accessToken is supplied, createWebSocket mints a fresh single-use ticket
(POST /v1/ws/ticket) before every connect and reconnect and carries it via the
Sec-WebSocket-Protocol header (['heimdall-ticket', ticket]) — the token never appears
in the WS URL. If minting fails it retries with backoff (bounded by
maxReconnectAttempts) and never opens an unauthenticated socket when a token was
expected. With no accessToken, it connects anonymously (public channels only). Because
the socket is created lazily after the async mint, socket is undefined until the first
connection is established — use the client methods rather than touching socket directly.
See WebSocket API → Authentication.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
config | ApiClientConfig | Yes | API client config (baseUrl, optional wsUrl) — use getApiConfig() |
options | WebSocketConfig | No | Connection options (token, reconnect behaviour) |
Connection Options
interface WebSocketConfig {
accessToken?: string;
autoReconnect?: boolean;
reconnectDelay?: number;
maxReconnectAttempts?: number;
}
| Option | Type | Default | Description |
|---|---|---|---|
accessToken | string | - | OAuth access token for auth |
autoReconnect | boolean | true | Auto-reconnect on disconnect |
reconnectDelay | number | 3000 | Delay between reconnects (ms) |
maxReconnectAttempts | number | 5 | Max reconnection attempts |
Returns
interface WebSocketClient {
socket?: WebSocket; // undefined until the first connection opens (lazy — minted async)
send: (data: unknown) => boolean;
readonly isOpen: boolean;
close: () => void;
onOpen: (handler: () => void) => void;
onMessage: (handler: (data: unknown) => void) => void;
onError: (handler: (error: Event) => void) => void;
onClose: (handler: (event: CloseEvent) => void) => void;
}
send returns true if the message was sent (socket open), false otherwise. Use the isOpen getter to check connection state, and onOpen to run logic once the connection is established.
Example
const ws = createWebSocket(getApiConfig(), {
accessToken: session.accessToken,
autoReconnect: true,
reconnectDelay: 5000,
maxReconnectAttempts: 3,
});
ws.onMessage((data) => {
console.log("Received:", data);
});
ws.onError((error) => {
console.error("WebSocket error:", error);
});
ws.onClose((event) => {
console.log("Connection closed:", event.code);
});
// Send a message
ws.send({ type: "Subscribe", channel: "updates" });
// Close when done
ws.close();
useWebSocket Hook
React hook for WebSocket connections with state management. Like createWebSocket, its
first argument is the ApiClientConfig — pass getApiConfig().
import { getApiConfig, useWebSocket } from "@/lib/api";
function LiveUpdates() {
const { isConnected, lastMessage, send, connect, disconnect } = useWebSocket(
getApiConfig(),
{ accessToken: session.accessToken }
);
return (
<div>
<p>Status: {isConnected ? "Connected" : "Disconnected"}</p>
<button onClick={() => send({ type: "ping" })}>Ping</button>
</div>
);
}
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
config | ApiClientConfig | Yes | API client config — use getApiConfig() |
options | UseWebSocketOptions | No | Connection options (extends WebSocketConfig with immediate) |
Options
interface UseWebSocketOptions extends WebSocketConfig {
immediate?: boolean; // Connect immediately (default: true)
}
Returns
interface UseWebSocketReturn {
isConnected: boolean;
lastMessage: unknown;
send: (data: unknown) => void;
connect: () => void;
disconnect: () => void;
}
Example with Message Handling
function NotificationListener() {
const { isConnected, lastMessage } = useWebSocket(getApiConfig(), {
accessToken: session.accessToken,
});
useEffect(() => {
if (lastMessage) {
const msg = lastMessage as { type: string; payload: unknown };
switch (msg.type) {
case "notification":
showNotification(msg.payload);
break;
case "update":
refreshData();
break;
}
}
}, [lastMessage]);
return <ConnectionStatus connected={isConnected} />;
}
Server Message Types
The library includes TypeScript types for all server-sent WebSocket messages.
User Status Messages
Sent when user account status changes:
import type { UserStatusMessage } from "@/lib/api";
// Account deleted
interface AccountDeletedMessage {
type: "accountDeleted";
userId: string;
reason?: string;
timestamp: string;
}
// Account banned
interface AccountBannedMessage {
type: "accountBanned";
userId: string;
reason?: string;
expiresAt?: string;
isPermanent: boolean;
timestamp: string;
}
// Session revoked
interface SessionRevokedMessage {
type: "sessionRevoked";
userId: string;
sessionId?: string;
reason?: string;
timestamp: string;
}
// Force logout
interface ForceLogoutMessage {
type: "forceLogout";
userId: string;
reason?: string;
timestamp: string;
}
Permission Messages
Sent when user permissions change:
import type { PermissionMessage } from "@/lib/api";
// Roles updated
interface RolesUpdatedMessage {
type: "rolesUpdated";
userId: string;
/** Role names (e.g., "Admin", "Developer") - for display */
roles: string[];
/** Role IDs (e.g., "role_admin", "role_developer") - for programmatic checks */
roleIds: string[];
timestamp: string;
}
// Permissions updated
interface PermissionsUpdatedMessage {
type: "permissionsUpdated";
userId: string;
permissions: string[];
timestamp: string;
}
// Role permissions changed (system-wide)
interface RolePermissionsChangedMessage {
type: "rolePermissionsChanged";
roleId: string;
roleName: string;
timestamp: string;
}
Account Link Messages
Sent when account linking events occur:
import type { AccountLinkMessage } from "@/lib/api";
// Email link verified
interface EmailLinkVerifiedMessage {
type: "emailLinkVerified";
userId: string;
email: string;
platformAccountId: string;
timestamp: string;
}
// Email change verified
interface EmailChangeVerifiedMessage {
type: "emailChangeVerified";
userId: string;
oldEmail: string;
newEmail: string;
timestamp: string;
}
OAuth & GPS Messages
Sent for OAuth consent changes and GPS/AIS/geofence tracking:
import type { OAuthConsentRevokedMessage, ServerMessage } from "@/lib/api";
// OAuth consent revoked for a client app
interface OAuthConsentRevokedMessage {
type: "oAuthConsentRevoked";
userId: string;
clientId: string;
clientName: string;
timestamp: string;
}
// The GPS messages are part of the exported `ServerMessage` union but are NOT
// exported as standalone names. Narrow them by switching on `type`:
// - "gpsUpdate" (GpsUpdateMessage) new GPS data point for a device
// - "vesselsUpdate" (VesselsUpdateMessage) current AIS vessel positions
// - "geofenceEvent" (GeofenceEventMessage) device entered/exited a geofence
Handling Server Messages
import type { ServerMessage } from "@/lib/api";
ws.onMessage((data: unknown) => {
const message = data as ServerMessage;
switch (message.type) {
case "accountBanned":
handleBan(message);
break;
case "forceLogout":
signOut();
break;
case "rolesUpdated":
refreshPermissions();
break;
case "emailLinkVerified":
refreshAccounts();
break;
case "oAuthConsentRevoked":
refreshConnectedApps();
break;
case "gpsUpdate":
updateDevicePosition(message.data);
break;
case "vesselsUpdate":
updateVessels(message.data.vessels);
break;
case "geofenceEvent":
handleGeofenceEvent(message.data);
break;
}
});
Best Practices
Cleanup on Unmount
Always close WebSocket connections when components unmount:
useEffect(() => {
const ws = createWebSocket(getApiConfig(), { accessToken });
ws.onMessage(handleMessage);
return () => {
ws.close(); // Important: cleanup
};
}, [accessToken]);
The useWebSocket hook handles this automatically.
Service-to-Service Identity Relay (?userId=)
createWebSocket / useWebSocket only ever carry the accessToken via a minted ticket
subprotocol — WebSocketConfig has no userId option. The server also accepts a
?userId=<uuid> override on the WS upgrade, but it's reserved for system API keys
connecting via a raw WebSocket client (e.g. the Discord bot / webapp relay), not this
browser-facing library. See WebSocket API → Authentication
for the full behavior — any non-system credential presenting ?userId= is rejected 403.
Reconnection Handling
The client auto-reconnects by default. Customize behavior:
const ws = createWebSocket(getApiConfig(), {
accessToken,
autoReconnect: true,
reconnectDelay: 5000, // Wait 5s between attempts
maxReconnectAttempts: 10, // Give up after 10 attempts
});
Connection State UI
function ConnectionIndicator() {
const { isConnected } = useWebSocket(getApiConfig(), { accessToken });
return (
<div className={isConnected ? "text-green-500" : "text-red-500"}>
{isConnected ? "Connected" : "Reconnecting..."}
</div>
);
}