> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# The Sync real-time shared-state model

> How Devotel Orbit Sync converges shared state across clients — the four object kinds, the REST-write to WebSocket-broadcast convergence loop, tenant scoping, TTL semantics, and the read/write permission split.

# The Sync real-time shared-state model

Sync is Orbit's real-time shared-state primitive — the answer to "several
clients need to see the same changing thing." A browser session, a mobile
app, and a backend worker can all read the same object and observe each
other's writes within milliseconds, without you running any pub/sub
infrastructure. The object model mirrors Twilio Sync, so an existing
Twilio Sync integration migrates as a path swap.

This page explains the model at concept level. For the endpoint-by-endpoint
walkthrough, see the [Sync real-time state guide](/guides/sync-realtime-state);
for the raw request/response shapes, the [Sync API reference](/api-reference/sync).

## 1. The four object kinds

Every Sync object is addressed by a `unique_name` you choose (letters,
digits, `.`, `_`, `:`, and `-`, up to 256 characters) and comes in one of
four kinds:

| Kind       | Durability | Shape                                                           | Use it for                                                                             |
| ---------- | ---------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `document` | Durable    | A single JSON object with a monotonically increasing `revision` | Singleton shared state — room configuration, a workflow's current step, a feature flag |
| `map`      | Durable    | An unordered key → JSON-value collection                        | Presence rosters, per-user state, keyed counters                                       |
| `list`     | Durable    | An ordered, zero-based-indexed JSON-value collection            | Append-only strips — an activity rail, a join log                                      |
| `stream`   | Ephemeral  | Pub/sub messages — nothing is stored                            | Live aggregates (viewer counts) and soft signals (typing indicators)                   |

The durable kinds persist until you delete them or their `ttl` expires. A
stream persists nothing: only clients subscribed at publish time receive a
message. That durability split is the first modeling decision — Sync is
built for coordination state, not the system of record. Keep authoritative
data in your own database; use Sync for the shared, changing view of it.

## 2. The convergence loop: REST write → WebSocket broadcast → rehydrate

Sync's architecture is a write path and a push path that meet at the client:

1. **REST is the only write path.** Every mutation — create, update,
   delete, publish — is a REST call against `/api/v1/sync`. There is no
   write-over-socket frame, so a write's authentication, authorization, and
   validation always go through the same API pipeline.
2. **A write publishes a change event.** The committed write fans an event
   out to the per-object channel.
3. **The WebSocket gateway relays the event to subscribers.** Clients
   connected to `/api/v1/ws/sync` and subscribed to that object receive an
   event envelope (`type`, `kind`, `unique_name`, `revision`, `data`, plus
   `key` for map events and `index` for list events). Update events carry
   the full new `data`; removal events carry none.
4. **Clients rehydrate over REST after a (re)connect.** The gateway is a
   notification channel, not a state source. After connecting or
   reconnecting, a client `GET`s the object's current state over REST, then
   applies streamed events on top. The same rehydration repairs anything
   the client missed while disconnected — the REST read is the convergence
   point for the durable kinds.

Two consequences follow. First, `revision` numbers (per object on
documents, per item on maps and lists) let a client detect a stale base: if
the reply revision moved past the one you held, another writer won, and you
converge on a fresh `GET` instead of retrying with a stale base. Second, a
stream never converges — by design. If late-joining clients must see a
value, put it in a map or document, not a stream.

### One presence map update, end to end

```mermaid theme={null}
sequenceDiagram
    participant Writer as Writing client
    participant API as Sync REST API
    participant GW as WebSocket gateway
    participant Sub as Live subscriber
    participant Late as Late-joining client

    Writer->>API: PUT /maps/room_42_presence/items/user_123
    API->>API: Commit item + bump revision
    API->>GW: Publish map.item.set event
    GW->>Sub: Event envelope (key, revision, data)
    Late->>API: GET /maps/room_42_presence/items
    API-->>Late: Current items (rehydration)
    Late->>GW: Re-subscribe, then apply events
```

The subscriber converges on the event; the late joiner converges on the
REST read. Both end at the same state.

## 3. Tenant scoping

Tenant scoping is absolute. Every object lives inside exactly one tenant,
bound at authentication: the API key or session token you present resolves
to a tenant, and every path you address is read inside that tenant's scope.
The WebSocket gateway applies the same binding — subscriptions can only
attach to objects in the authenticated tenant. One tenant can never read
another tenant's objects, and there is no addressing form that crosses the
boundary.

Sync imposes no platform-level gate on what you share inside your tenant.
Compliance controls (quiet hours, consent, suppression) are tenant-owned
configuration elsewhere in the platform; shape your own usage policy on the
object.

## 4. TTL and expiration semantics

* `ttl` is a seconds value (integer, maximum one year) attached to a
  document or map write. `ttl: 0` is the default and means **no expiry** —
  the object persists until you `DELETE` it.
* Setting a new `ttl` on a write resets the expiry window from that write.
  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, because a crashed client then
  drops off the roster without a cleanup pass. Use `ttl: 0` for state you
  manage explicitly, and delete it when the workflow ends.
* A stream has no retention at all: a client that was not subscribed at
  publish time never sees the message, no matter when it connects.

## 5. The read/write permission split

Sync splits access by operation, tied to organization roles:

* **Reads** — `GET` on any object family, and WebSocket subscriptions — are
  open to **any authenticated member** of the organization.
* **Writes** — create, update, delete, and stream publish — require an
  **operator role**: `owner`, `admin`, `developer`, or `agent`. The
  dashboard's `member` role can read but not write.

The practical consequence for client applications: anything you embed is a
subscriber unless you deliberately make it a writer. Never ship an
operator-role API key to end users; let browsers and mobile clients
subscribe with scoped session tokens, and keep writes on your backend or on
server-minted keys.

## 6. When to use Sync — and when not to

Orbit has several real-time surfaces. Pick Sync when the shape is *shared
state*, not *event delivery*:

* **Inbox** — a durable alert feed where the item itself is the artifact
  and items accumulate. Sync objects get overwritten or expire; inbox items
  accumulate. If the history matters, it is an inbox.
* **Notifications** — point-in-time alerts to an operator. If the event is
  the message, it is a notification, not shared state.
* **Webhooks** — server-to-server push to your endpoint. Prefer Sync when
  the consumers are client applications (browser, mobile, desktop) that you
  do not want polling; prefer webhooks when the consumer is your backend.
* **Async work** — Sync is live coordination and carries no durability
  guarantee for *work*. A job that must survive a process restart belongs
  on the queue backbone, not in a Sync object — see
  [how Orbit processes work asynchronously](/concepts/async-processing-model).

The rule of thumb: if clients converge on a value, it is Sync; if an event
must be delivered, it is a webhook or a notification; if items accumulate,
it is an inbox; if work must survive a restart, it is a queue.

## See also

* [Sync real-time state guide](/guides/sync-realtime-state) — the full
  REST + WebSocket walkthrough with a runnable end-to-end example
* [Sync API reference](/api-reference/sync) — endpoint-by-endpoint shapes
* [Endpoint reference (auto-generated)](/api-reference/endpoints/sync)
* [How Orbit processes work asynchronously](/concepts/async-processing-model) —
  the queue backbone for work that must survive restarts
* [Tenant isolation](/concepts/tenant-isolation) — the scoping guarantee
  Sync's object model builds on
