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"
}
}
| Field | Required | Description |
|---|---|---|
content | yes | User's message text |
conversationId | no | Existing 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": {}
}
}
| Field | Description |
|---|---|
id | Notification database ID |
title | Display title |
body | Display body text |
level | "info" | "warning" | "critical" |
category | Push notification category for action buttons |
data | Extra 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
}
}
| Field | Description |
|---|---|
mediaId | Media file database ID |
url | URL to stream/download the file |
title | Display title |
type | "video" | "audio" | "image" |
position | Resume 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
};