WebSocket channels
Mark any interface with ws: "on" and it's reachable over WebSocket at
wss://<host>/ws/<ORG_UUID>/<env>/<interface>. Clients open one socket and drive channels with small
JSON control frames; pushes arrive out-of-band on the same socket.
name: RealtimeApp
interfaces:
live:
ws: "on" # realtime — no `output` needed
subscribe_authorizer: authorize-channel # optional per-channel ACL (see Auth)
# output: http # optional — also expose over HTTP (dual interface)
actions:
- name: Ping
input: a|body|
Each non-control frame runs the interface (request/response) and the reply comes back on the socket.
ws: "on" is all that's required — see Dual interface for
adding output: http to serve the same interface over HTTP too.
Control frames (client → server)
| Frame | Effect |
|---|---|
{"ap_subscribe": "room:42"} or {"ap_subscribe": ["a","b"]} | Subscribe. Wildcards: room:+ (one level), room:# (all). Reply: {"ap_subscribed":[…]} |
{"ap_subscribe": ["room:42"], "history": 20} | Subscribe and replay the last 20 messages (see History) |
{"ap_unsubscribe": "room:42"} | Unsubscribe |
{"ap_publish": {"channels":["room:42"], "data":{…}, "retain": true}} | Publish. retain: true stores it as the channel's last value for future subscribers. Reply: {"ap_published":[…], "delivered": N} |
{"ap_presence_enter": {"channel":"room:42", "id":"alice", "state":{…}}} | Join presence (see Presence) |
{"ap_presence_leave": {"channel":"room:42"}} | Leave presence (also automatic on disconnect) |
{"ap_presence_get": {"channel":"room:42"}} | List members: {"ap_presence":{"channel":"room:42","members":[…]}} |
Pushes arrive as {"ap_channel":"room:42","data":{…}}. A denied/failed op replies {"ap_error":"…"}.
deliveredis a channel count, not a live subscriber count — the router forwards asynchronously (the same model realtime specialists use).
Publishing from a pipeline
Any interface (HTTP, MQTT, scheduled) can push to channels with a ws_publish action — e.g. a
database insert that also fans the new row out to subscribers:
- name: Broadcast
ws_publish:
channels: a|body::channels|
data: a|body::data|
retain: a|body::retain| # literal true/false or an a|…| marker
Presence
Presence tracks who is on a channel and emits enter/leave events — the Ably/Pusher parity feature, bundled and queryable in your pipeline.
ws.send(JSON.stringify({ ap_presence_enter: { channel: "room:42", id: "alice", state: { typing: false } } }));
ws.send(JSON.stringify({ ap_presence_get: { channel: "room:42" } }));
Subscribers to the channel receive presence events as
{"ap_channel":"room:42","data":{"$presence":{"action":"enter","id":"alice","state":{…}}}}. The member
list from ap_presence_get is cluster-wide (gathered across nodes). Members auto-leave on disconnect.
History-rewind
Add "history": N to a subscribe to replay the last N messages for each exact channel (wildcards
aren't replayed). Each replayed frame is flagged "ap_history": true so the client can tell replay
from live:
ws.send(JSON.stringify({ ap_subscribe: ["room:42"], history: 50 }));
// → {"ap_channel":"room:42","data":{…},"ap_history":true} ×(up to 50), then live frames
History depth is a per-org entitlement (max_ws_history) — see Limits.
Delivery
Ordered per channel; best-effort live delivery plus retained-value recovery and history-rewind so a (re)connecting client isn't left blank. Cross-node delivery is at-least-once within the mesh retry window. A slow consumer's outbound queue is bounded (drop-on-full), so one socket can't grow node memory without limit.
Next: MQTT broker → · Auth →