Skip to main content

WebSocket Message Reference

Complete reference for all WebSocket message types. Messages flow over the /ws endpoint after JWT authentication.

Envelope Format

Every message uses this JSON envelope:

{
id: string; // Unique message ID
type: string; // Domain-qualified type
payload: object; // Domain-specific data
deviceId?: string; // Set by server on incoming messages
timestamp?: number; // Unix milliseconds
}

Ping Domain

ping.request (client → server)

Heartbeat check.

{
"id": "p1",
"type": "ping.request",
"payload": {}
}

ping.response (server → client)

{
"id": "p1",
"type": "ping.response",
"payload": { "time": 1705312800000 }
}

Chat Domain

chat.message (client → server)

Send a chat message.

{
"id": "m1",
"type": "chat.message",
"payload": {
"content": "What's the weather like?",
"conversationId": "uuid"
}
}
FieldRequiredDescription
contentyesUser's message text
conversationIdnoExisting conversation ID. If omitted, a new conversation is created

chat.delta (server → client)

Streaming response token.

{
"type": "chat.delta",
"payload": {
"conversationId": "uuid",
"content": "Based on ",
"role": "assistant"
}
}

Multiple chat.delta messages are sent as tokens are generated.

chat.done (server → client)

Marks the end of a streaming response.

{
"type": "chat.done",
"payload": {
"conversationId": "uuid",
"messageId": "uuid"
}
}

chat.tool_call (server → client)

Indicates the AI is executing a tool.

{
"type": "chat.tool_call",
"payload": {
"conversationId": "uuid",
"toolName": "capture_camera",
"status": "executing"
}
}

chat.error (server → client)

Chat processing error.

{
"type": "chat.error",
"payload": {
"conversationId": "uuid",
"error": "Ollama is not responding"
}
}

Notification Domain

notification.push (server → client)

Deliver a notification to the device.

{
"type": "notification.push",
"payload": {
"id": "uuid",
"title": "Camera Alert",
"body": "Motion detected in backyard",
"level": "warning",
"category": "CAMERA_ALERT",
"data": {}
}
}
FieldDescription
idNotification database ID
titleDisplay title
bodyDisplay body text
level"info" | "warning" | "critical"
categoryPush notification category for action buttons
dataExtra payload (e.g., camera ID, media ID)

notification.ack (client → server)

Acknowledge receipt of a notification. Updates status to delivered.

{
"id": "a1",
"type": "notification.ack",
"payload": { "notificationId": "uuid" }
}

notification.read (client → server)

Mark a notification as read.

{
"id": "r1",
"type": "notification.read",
"payload": { "notificationId": "uuid" }
}

notification.list (client → server)

Request pending notifications. Server responds with multiple notification.push messages.

{
"id": "l1",
"type": "notification.list",
"payload": { "status": "pending" }
}

Media Domain

media.play (server → client)

Command a device to play media.

{
"type": "media.play",
"payload": {
"mediaId": "uuid",
"url": "/api/media/uuid",
"title": "Movie.mp4",
"type": "video",
"position": 1234.5
}
}
FieldDescription
mediaIdMedia file database ID
urlURL to stream/download the file
titleDisplay title
type"video" | "audio" | "image"
positionResume position in seconds (optional)

Plugin Domain

plugin.widget.subscribe (client → server)

Subscribe to live widget data updates for one or more plugins. The server immediately sends the current cached payload for each requested slug, then pushes updates whenever a plugin calls genie.widget.invalidate().

{
"id": "w1",
"type": "plugin.widget.subscribe",
"payload": {
"slugs": ["prayer-times", "weather"]
}
}

plugin.widget-data (server → client)

Pushed to all subscribers whenever a plugin's widget data changes.

{
"type": "plugin.widget-data",
"payload": {
"slug": "prayer-times",
"data": {
"nextPrayer": "Asr",
"nextPrayerTime": "15:42",
"remaining": "1h 12m"
}
}
}

plugin.widget.bundle-changed (server → client)

Sent in dev mode when a plugin's compiled widget.js file changes on disk. Clients should re-import the bundle without a full page reload.

{
"type": "plugin.widget.bundle-changed",
"payload": {
"slug": "prayer-times",
"v": 2
}
}

plugin.tablet.sound (server → client)

Targeted at a specific tablet. Instructs the device to fetch and play an audio asset from the plugin's asset route.

{
"type": "plugin.tablet.sound",
"payload": {
"slug": "prayer-times",
"url": "/api/plugins/prayer-times/assets/adhan.mp3",
"durationMs": 90000
}
}

durationMs is optional. The tablet plays the sound via expo-av and interrupts any currently playing plugin sound.


Error Messages

error (server → client)

Generic error response for unroutable or malformed messages.

{
"type": "error",
"payload": {
"message": "No handler for domain: unknown",
"originalType": "unknown.something"
}
}

Client Implementation Guide

Connecting

const ws = new WebSocket(`ws://server:3000/ws?token=${jwt}`);

ws.onopen = () => {
console.log("Connected");
// Send initial ping
ws.send(JSON.stringify({
id: crypto.randomUUID(),
type: "ping.request",
payload: {},
}));
};

Sending Messages

function send(type: string, payload: object) {
ws.send(JSON.stringify({
id: crypto.randomUUID(),
type,
payload,
timestamp: Date.now(),
}));
}

// Send a chat message
send("chat.message", {
content: "Hello!",
conversationId: currentConversationId,
});

Handling Responses

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

switch (msg.type) {
case "chat.delta":
appendToChat(msg.payload.content);
break;
case "chat.done":
finishChat(msg.payload.messageId);
break;
case "chat.tool_call":
showToolIndicator(msg.payload.toolName);
break;
case "notification.push":
showNotification(msg.payload);
break;
case "media.play":
startPlayback(msg.payload);
break;
case "error":
handleError(msg.payload.message);
break;
}
};

Reconnection

The server terminates connections that fail heartbeat (30s ping, 10s timeout). Clients should implement reconnection with exponential backoff:

let reconnectDelay = 1000;

ws.onclose = () => {
setTimeout(() => {
connect();
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
}, reconnectDelay);
};

ws.onopen = () => {
reconnectDelay = 1000; // Reset on successful connection
};