Skip to main content
Devotel Orbit Sync is a real-time shared-state primitive: when several clients (a browser session, a mobile app, a backend worker) need to read and change the same piece of state and see each other’s writes as they happen, Sync gives you that convergence without your own pub/sub infrastructure. It mirrors the Twilio Sync object model, so a migration is a path swap. This guide walks the full stack: when to reach for Sync, the REST surface object by object, the WebSocket subscription protocol, a complete presence + counter example, and the TTL, error, and permission semantics you need on every retry loop.

1. What Sync is

Sync exposes four object kinds, all addressed by a unique_name you choose (letters, digits, ., _, :, and -, up to 256 characters) and all scoped to your tenant — one tenant can never read or address another tenant’s objects: The durable kinds persist until you delete them (or a ttl you set expires). Every REST write publishes a change event to a per-object channel; the WebSocket gateway relays those events to subscribed clients, and clients re-hydrate current state through the REST GET endpoints after a (re)connect. Objects live in a low-latency store chosen for fast, frequent mutation — Sync is for coordination state, not the system of record. Keep authoritative data in your own database and use Sync for the shared, changing view of it.

2. Sync vs. Inbox, Notifications, and Webhooks

Orbit has several real-time-ish surfaces. Pick Sync when the shape is shared state, not notification:
  • Inbox — a durable alert feed (“case 123 needs a review”) where the item itself is the durable artifact. Sync documents expire or get overwritten; inbox items accumulate.
  • Notifications — point-in-time alerts to an operator (“threshold crossed”). If the event is the message, that is a notification, not shared state.
  • Webhooks — server-to-server push to your backend. Use Sync when the consumers are client applications (browser, mobile, desktop) that you do not want polling.

3. REST walkthrough

Every request carries an X-API-Key header (or a session Bearer token). Reads are open to any authenticated member; writes require an operator role (owner, admin, developer, or agent).

Documents

Create a document (201 on success; a duplicate unique_name returns 409):
Fetch it — the response carries the current data and revision:
Update it. Revisions are monotonic, so a client uses them as an optimistic guard: read, write, and re-read; if the reply revision moved past the one you held, another writer won the race and you should re-hydrate instead of retrying with your stale base:
Delete returns 204 and emits a document.removed event:

Maps

A Map is an unordered key → JSON-value collection. Set an item (create-or-replace), then list the map:
Each item carries its own revision. DELETE a single item or the whole Map (204 either way, with map.item.removed / map.removed events).

Lists

A List is append-indexed. Appending returns the new item’s index (zero-based):
Update an item at a given index (PUT …/items/:index), remove it (DELETE …/items/:index), or delete the whole List (DELETE …/:name). All emit the corresponding list.item.* / list.removed events.

Streams

A Stream is ephemeral: publish a message and every client subscribed to the stream at that moment receives it. Nothing is stored; a client that connects after the publish does not see it. Useful for live counters and presence aggregates you do not want to persist:

4. WebSocket gateway

Point your client at the gateway and authenticate, then manage per-object subscriptions over the socket:
Server-side (Node): send the key in a header. Browsers cannot set WebSocket headers, so pass them as subprotocols (see below).
Browser: offer a marker protocol first plus the credential as an auth subprotocol — Bearer.<jwt> for a session token or ApiKey.<key> for an API key. The server scans the offered list for the auth token. Never embed a live API key in code shipped to end users; mint scoped session tokens on your backend.
After connect, the server sends { "type": "hello", "tenantId", ... }. Subscribe and unsubscribe with JSON frames:
Change events arrive as full envelopes:
The type is one of document.updated, document.removed, map.item.set, map.item.removed, map.removed, list.item.added, list.item.updated, list.item.removed, list.removed, or stream.message. revision is absent for streams; key appears on map events; index on list events; data is absent on removal events. A client ping frame gets a pong; malformed frames get an error frame. Liveness and limits. The server pings every 30 seconds and closes a socket after 90 seconds without activity (about three missed pongs). Replicate the client-side close logic: on close, re-hydrate state with a REST GET and re-subscribe. Two cluster-wide caps apply per tenant: at most 50 simultaneous sockets, and at most 200 subscribed objects per socket. If you exceed either, the server returns an error frame (too_many_connections / too_many_subscriptions) and closes the socket.

5. End-to-end example: chat-room presence + a live viewer counter

A common pairing: a durable Map for the per-user presence roster (survives reconnects) and an ephemeral Stream for the “current viewer count” badge (no persistence means no cleanup). The client below runs in Node or a browser — keep the key out of shipped front-end code; in the browser variant use the subprotocol auth from Section 4.
In a real deployment the backend (not the browser) usually publishes the viewer aggregate into the Stream — for an always-on badge your client just renders the stream message it receives.

6. TTL and retention semantics

  • ttl (seconds, integer, max one year) means the object’s data expires after that many seconds. ttl: 0 (the default) means no expiry — the object persists until you DELETE it.
  • On a Document and a Map you supply ttl per object/write; resetting it sets a fresh expiry window. Once the window elapses the object vanishes: reads return SYNC_OBJECT_NOT_FOUND, and subscriptions on it stop receiving updates.
  • Use a short ttl on anything whose absence should clean up automatically (presence entries are the canonical case). Use ttl: 0 for state you manage explicitly, and delete it when the workflow ends.
  • A Stream has no retention at all — non-subscribers at publish time never see the message.

7. Error taxonomy

Sync surfaces the same error envelope as the rest of the API (error.code / error.message / error.status / optional details). The relevant codes: The per-family limit is 120 requests/minute per operation family (reads vs. writes) — the details of that limiter live in the rate-limits guide.

8. Permissions

Tenant scoping is absolute: a client only ever addresses objects inside its own tenant, and the WebSocket gateway binds subscriptions to the authenticated tenant’s scope.
  • Reads (GET on any object family): any authenticated member of the organization.
  • Writes (create / update / delete / publish): the owner, admin, developer, or agent role. The dashboard’s member role can read but not write; mint scoped server-side keys for the writers your app embeds.
Compliance controls (quiet-hours, consent, DNC) are tenant-configured in Devotel Orbit and default to open — Sync does not impose platform-level gates on what you share; shape your own usage policy on the object.

See also