The MeshSocket Protocol

MeshSocket is a thin convention on top of WebSocket. Every message is a JSON frame; the relay dispatches on the frame's type. This page is the wire contract — everything in Data Flow — sync & actions is built from these primitives. For a terse lookup, see Message Reference.

The frame

Every message in either direction is:

{ "id": "uuid", "type": "string", "payload": <any>, "reply_to": "uuid | null" }
  • id — unique per frame; used to correlate a reply.
  • type — the verb. The relay has a fixed set of built-in verbs (below); for routed and broadcast messages, the application-level type travels inside the payload (e.g. msg_type).
  • payload — arbitrary JSON.
  • reply_to — when answering a request, set to the original frame's id.

You rarely build frames by hand — the client library wraps them as send, emit, request, and on. But knowing the shape lets you implement the protocol in any language (Building a Server in Any Language (Raw Protocol)).

Connecting and identifying

  1. Open a WebSocket to the relay URL.
  2. Send an identify frame announcing who you are and what you may do.
  3. The relay replies with welcome ({ id, name }) once you're admitted.

The identify payload carries:

Field Meaning
name Display name (also used as a routing handle)
channel The channel to join (default "default")
role Role label — also sets capability defaults
can_broadcast May send broadcast_request
can_route May send route_msg / route_msg_noreply
can_monitor Receives the member roster; may call get_nodes
broadcast_scope "global" or "channel" (default channel; global for monitors)
auth_token Gateway token (omit/empty for a self-hosted relay)

Capability defaults are role-based, so a hub/controller/node can typically broadcast and route, while a viewer only listens. Set them explicitly on your server — a data-pushing server needs can_broadcast: true, and a server that answers routed requests needs nothing special (handlers receive routed frames regardless), but to send routed requests you need can_route: true.

Gateway note: on the Connect+ gateway the socket opens and then waits for the gateway to admit it ("awaiting gateway"). An empty/invalid token leaves you parked — you connect but never join the mesh. Self-hosted relays have no such step. See Connection & Pairing.

The built-in verbs

These are the type values the relay itself understands.

broadcast_requestbroadcast

Fan-out. A member with can_broadcast sends:

{ "type": "broadcast_request", "payload": { "msg_type": "metrics", "cpu": 73 } }

The relay re-emits to every other member of the channel (respecting broadcast_scope) as:

{ "type": "broadcast", "payload": { "msg_type": "metrics", "cpu": 73 } }

So receivers listen for type: "broadcast" and demultiplex on a field inside the payload (by convention msg_type). This is the backbone of telemetry — see Data Flow — sync & actions.

route_msg (request / reply)

Point-to-point with a response. The sender (needs can_route) targets another member by target_id (preferred) or target_name:

{ "type": "route_msg",
  "payload": { "target_id": "uuid", "type": "get_doc", "payload": { "path": "x" } } }

The relay forwards { type: "get_doc", payload: {…} } to the target as a request, waits (≈5s) for the target's handler to return a value, and routes that value back to the sender as the reply. Use this when you need an answer (e.g. fetching markdown for a tapped graph node).

route_msg_noreply

Same routing, fire-and-forget (targets by target_name). No response is returned.

get_nodes

Returns the current channel roster to a can_monitor caller:

{ "clients": [ { "id", "name", "channel", "role" }, … ] }

This is the reliable way to discover peers. (A roster is also pushed as server_client_list on membership changes, but that push races client registration and can miss peers that join after you — poll get_nodes instead if you need to wait for someone to appear.)

welcome / server_client_list

Server-initiated frames you may receive: welcome on admission, and server_client_list ({ clients: [...] }) when the roster changes (monitors only — see the caveat above).

Request/response semantics

request(type, payload) sends a frame, registers id as pending, and resolves when a frame with reply_to == id arrives, or rejects on timeout (~5s default). On the receiving side, a handler registered for that type returns a value; the library wraps it into a reply_to frame automatically.

What this means for your server

  • To drive displays: identify with can_broadcast: true, then send broadcast_request frames carrying a msg_type. (Recipe: Telemetry Dashboard)
  • To receive inputs: just be on the channel — controls broadcast their actions (broadcast_request with a msg_type), so listen for broadcast and demux. Or, for targeted actions, register a handler for the routed type the control sends via route_msg. (Recipe: Interactive Controls)
  • To answer requests (e.g. content for a graph node): register a handler for the request type and return the response object. (Recipe: Graph + Request/Reply Content)

Next: Data Flow — sync & actions.