TERMPOD Docs

Control Messages

JSON control message specification for session management, key exchange, WebRTC signaling, and client presence.

Control messages are sent as JSON text frames on WebSocket connections. They handle authentication, session management, E2E key exchange, WebRTC signaling, and client presence.

Session WS Messages

These messages are exchanged on the per-session WebSocket (/sessions/:id/ws).

Client → Relay

hello

Sent immediately after WebSocket connection is established.

{
  "type": "hello",
  "version": 1,
  "role": "desktop" | "viewer",
  "device": "macos" | "iphone" | "ipad" | "browser",
  "clientId": "uuid-v4"
}

key_exchange

Sent by desktop to initiate E2E encryption with a viewer.

{
  "type": "key_exchange",
  "publicKey": { "kty": "EC", "crv": "P-256", "x": "base64url", "y": "base64url" },
  "toClientId": "viewer-uuid"
}

key_exchange_ack

Sent by viewer in response. After both sides exchange public keys, they derive the shared AES-256-GCM key via HKDF-SHA256.

{
  "type": "key_exchange_ack",
  "publicKey": { "kty": "EC", "crv": "P-256", "x": "base64url", "y": "base64url" },
  "toClientId": "desktop-uuid"
}

input_lock_request / input_lock_release

Request or release exclusive input rights.

{ "type": "input_lock_request", "clientId": "uuid-v4" }
{ "type": "input_lock_release", "clientId": "uuid-v4" }

ping

Application-level keepalive (in addition to WebSocket protocol pings).

{ "type": "ping", "timestamp": 1709654400000 }

Relay → Client

session_info

Sent after hello. Provides session metadata. Note: name and cwd are always empty strings on the relay — real session metadata is delivered E2E encrypted via encrypted_control.

{
  "type": "session_info",
  "sessionId": "session-abc123",
  "name": "",
  "cwd": "",
  "ptySize": { "cols": 120, "rows": 40 },
  "createdAt": "2026-03-05T10:00:00Z",
  "clients": [
    { "clientId": "uuid-1", "role": "desktop", "device": "macos", "connectedAt": "..." }
  ]
}

ready

Sent after all scrollback chunks have been delivered.

{ "type": "ready" }

client_joined / client_left

Broadcast when clients connect to or disconnect from the session.

{ "type": "client_joined", "clientId": "uuid-v4", "role": "viewer", "device": "iphone" }
{ "type": "client_left", "clientId": "uuid-v4", "reason": "closed" | "timeout" | "error" }

pty_resize

Broadcast when the desktop resizes the terminal.

{ "type": "pty_resize", "cols": 120, "rows": 40 }

session_ended

Sent when the desktop PTY exits or disconnects.

{ "type": "session_ended", "reason": "pty_exit" | "desktop_disconnected", "exitCode": 0 }

error

{
  "type": "error",
  "code": "AUTH_FAILED" | "SESSION_NOT_FOUND" | "RATE_LIMITED" | "INTERNAL",
  "message": "Human-readable description"
}

Device WS Messages

These messages are exchanged on the per-device WebSocket (/devices/:deviceId/ws).

Authentication

auth / auth_ok

{ "type": "auth", "token": "JWT_ACCESS_TOKEN" }
{ "type": "auth_ok" }

Handshake

hello (device)

{
  "type": "hello",
  "role": "desktop" | "viewer",
  "device": "macos" | "iphone" | "ipad",
  "clientId": "uuid-v4",
  "version": 1
}

hello_ok

{
  "type": "hello_ok",
  "clients": [
    { "clientId": "uuid", "role": "desktop", "device": "macos", "connectedAt": "ISO-8601" }
  ]
}

Session Management

list_sessions / sessions_list

{ "type": "list_sessions" }
{
  "type": "sessions_list",
  "sessions": [
    { "id": "uuid", "ptyCols": 120, "ptyRows": 40, "createdAt": "ISO-8601" }
  ]
}

The relay only returns non-sensitive fields (ID, dimensions, timestamps). Session metadata (name, cwd, processName) is delivered E2E encrypted via encrypted_control.

create_session_request / session_created

{ "type": "create_session_request", "requestId": "uuid" }
{
  "type": "session_created",
  "requestId": "uuid",
  "sessionId": "new-session-uuid",
  "name": "project-name",
  "cwd": "/path",
  "ptyCols": 120,
  "ptyRows": 40
}

session_created is sent E2E encrypted via encrypted_control.

delete_session / session_closed

{ "type": "delete_session", "sessionId": "uuid" }
{ "type": "session_closed", "sessionId": "uuid" }

E2E Key Exchange

Same format as Session WS, but routed through the User DO:

{
  "type": "key_exchange",
  "publicKey": { "kty": "EC", "crv": "P-256", "x": "base64url", "y": "base64url" },
  "toClientId": "viewer-uuid"
}

Local Auth

local_auth_secret

Sent by desktop to mobile viewers, wrapped in encrypted_control (E2E encrypted). Contains the auth secret for Bonjour connections.

{ "type": "local_auth_secret", "secret": "random-auth-secret" }

WebRTC Signaling

WebRTC signaling is routed through the Device WS with toClientId targeting.

webrtc_offer

{
  "type": "webrtc_offer",
  "toClientId": "target-uuid",
  "from": "source-uuid",
  "offer": { "type": "offer", "sdp": "v=0\r\no=- ..." }
}

webrtc_answer

{
  "type": "webrtc_answer",
  "toClientId": "target-uuid",
  "from": "source-uuid",
  "answer": { "type": "answer", "sdp": "v=0\r\no=- ..." }
}

webrtc_ice

{
  "type": "webrtc_ice",
  "toClientId": "target-uuid",
  "from": "source-uuid",
  "candidate": { "candidate": "candidate:...", "sdpMLineIndex": 0 }
}

ICE servers: Google STUN (stun:stun.l.google.com:19302, stun:stun1.l.google.com:19302) and Cloudflare STUN (stun:stun.cloudflare.com:3478). Optional Cloudflare TURN credentials are fetched from GET /turn-credentials before each WebRTC connection attempt.


Local WS Messages (Bonjour P2P)

auth

Must be the first message on the local WebSocket. The desktop validates the secret before accepting anything else.

{ "type": "auth", "secret": "random-auth-secret" }

The secret is obtained via the local_auth_secret Device WS message.

subscribe_session / unsubscribe_session

{ "type": "subscribe_session", "sessionId": "uuid" }
{ "type": "unsubscribe_session", "sessionId": "uuid" }

Session management messages (list_sessions, sessions_list, create_session_request, delete_session, session_closed) also work over the local WS.

On this page