ServicesLive JSON

Real-time Subscriptions

Each document has its own WebSocket endpoint. A client opens the socket, picks JSONPaths to watch, and receives a frame for every matching change. Everything on this page - the handshake, each frame type, and the change delivery - was captured from a live server.

The endpoint

WS /ws/live-json/{document_id}

Mounted at the server root - not under /api/v1. Authenticate with the usual Authorization: Bearer header, or with a ?token= query parameter for browser clients that cannot set headers. A missing or bad token fails the upgrade with 401; a token without the live-json scope fails with 403.

Watching from the CLI

watch opens the socket, subscribes to a path, and streams every frame until interrupted. Captured live while a patch ran in a second shell:

snug live-json watch -d doc_vgDjAtvHYhar -p '$.count'
Watching 'doc_vgDjAtvHYhar' path '$.count' (press CTRL+C to stop).
{"type":"connection_established","connection_id":"GYKMymuvcVVS","document_id":"doc_vgDjAtvHYhar"}
{"type":"subscribed","subscription_id":"sub_tWmYNJsFFNNr","path":"$.count","initial_value":6}
{"type":"live_json_change","data":{"subscription_id":"sub_tWmYNJsFFNNr","document":"doc_vgDjAtvHYhar","path":"$.count","operation":"replace","value":7,"old_value":6,"timestamp":"2026-08-28T04:35:22.083515Z"}}

The subscribed acknowledgement carries initial_value - the current value at the path - so a client renders immediately without a separate read.

The socket protocol

Messages are JSON text frames tagged with type. Client to server:

{"type": "subscribe", "path": "$.count"}
{"type": "unsubscribe", "subscription_id": "sub_tWmYNJsFFNNr"}
{"type": "ping"}

Server to client, all reproduced live:

  • connection_established - first frame after the upgrade; carries the connection_id used by the HTTP subscribe API below.
  • subscribed - acknowledges a subscribe, with subscription_id, path, and initial_value.
  • unsubscribed - acknowledges an unsubscribe.
  • live_json_change - a change at a subscribed path (shape above).
  • pong - answers ping.
  • error - a client message the server could not process.

One socket watches one document (fixed by the URL) but can hold many path subscriptions. Use $ as the path to watch the whole document.

Change frames report the new value at the path

The frame carries the resulting value at the subscribed path, not a diff. An append patch to a watched array arrives as "operation": "replace" with the entire new array - captured live:

{"type":"live_json_change","data":{"subscription_id":"sub_nbjaXTVGSFGp","document":"doc_vgDjAtvHYhar","path":"$.items","operation":"replace","value":["a","b","c"],"old_value":["a","b"],"timestamp":"2026-08-28T04:36:19.486987Z"}}

The HTTP subscribe API is socket-gated

POST /api/v1/live-json/subscribe registers a path watch on an already open socket - it is an alternative to sending subscribe as a socket frame, useful when a separate process manages subscriptions. Pass the connection_id from the handshake:

snug live-json subscribe -d doc_vgDjAtvHYhar -p '$.items' -c VUJtsQpmacbf
snug live-json subscriptions -d doc_vgDjAtvHYhar
snug live-json unsubscribe -s sub_nbjaXTVGSFGp

Verified live: the subscribe response echoes subscription_id, initial_value, and connection_id, and subsequent changes arrive on the socket exactly as with a socket-frame subscribe. Without a live socket it refuses:

  • No -c at all: 400 "connection_id is required: open WS /ws/live-json/{document} and subscribe with its connection_id".
  • A connection_id that is not an open socket: 400 "connection_id is not an open live-json socket".

Subscriptions die with their socket - after the connection closes, unsubscribe for its subscription returns 404 subscription_not_found.

On this page