# Realtime sync and live collaboration

A glow's stores broadcast: every time a key is set or deleted, connected
sessions hear about it. Wire that into your app and two people looking
at the same board see each other's moves without anyone polling or
reloading. This guide is the recipe for building that — subscribing,
refetching, and (the half everyone forgets) **writing so that concurrent
edits merge instead of clobbering each other**. Live UI without safe
writes just makes the lost updates visible; the two halves ship
together.

The runtime facts each half rests on are documented once in the
reference — the event surface under
[`backlit.on`](https://sdk.backlit.run/sdk.md#backliton--realtime-subscriptions)
(event shape, which sessions receive which events, delivery guarantees,
reconnects) and the safe-write loop under
[saving shared state safely](https://sdk.backlit.run/sdk.md#saving-shared-state-safely).
This guide links to both rather than repeating them, so it can't drift
out of sync with them.

## The mental model

An event is a **hint, not the data**. What arrives names the store, the
operation, the key, and an integrity hash of the new value — never the
value itself (the exact shape is in
[`backlit.on`](https://sdk.backlit.run/sdk.md#backliton--realtime-subscriptions)).
Your app reacts by refetching what it actually cares about. That keeps
the loop simple and honest:

1. **Seed** — read or list the state you display.
2. **Subscribe** — on a matching event, refetch that key and re-render.
3. **Write safely** — anything multiple people edit goes through
   `updateJSON`, so a concurrent edit rebases instead of being lost.

Delivery is **best-effort**: an event can be missed across a reconnect
and can occasionally arrive twice — the guarantees and the recommended
compensations are spelled out in the reference's delivery-semantics
notes under
[`backlit.on`](https://sdk.backlit.run/sdk.md#backliton--realtime-subscriptions).
Design for it by treating events as cache invalidation, never as the
source of truth.

## A worked example — a live team board

Sticky notes on a shared retro board: anyone signed in posts a note,
everyone sees new notes appear live, and each note shows its author.
Notes are owned content — one person posts, others read but don't edit —
so they live in the
[records store](https://sdk.backlit.run/sdk.md#backlitrecords--public-read-owner-write-store)
under one key per note, and each event carries the note's owner.

```html
<script src="https://sdk.backlit.run/sdk/v1/sdk.js"></script>
<script>
  (async () => {
    const me = await backlit.auth.me();  // records needs a signed-in user
    const notes = new Map();             // key → { value, crc32c, owner }

    async function refresh(key) {
      try {
        const { value, crc32c, owner } = await backlit.records.getJSON(key);
        notes.set(key, { value, crc32c, owner });
      } catch (e) {
        if (e.code !== "not_found") throw e;  // deleted between event and fetch
        notes.delete(key);
      }
      render();
    }

    // Subscribe BEFORE seeding, so a write that lands mid-listing isn't
    // missed — refresh() is idempotent, so an early event is harmless.
    const off = backlit.on("storage.change", (e) => {
      if (e.store !== "records" || !e.key.startsWith("note.")) return;
      if (e.operation === "delete") { notes.delete(e.key); render(); return; }
      const have = notes.get(e.key);
      if (have && have.crc32c === e.crc32c) return;  // duplicate delivery — skip
      refresh(e.key);                                // fetch only the changed key
    });

    // Seed once from a listing; from here on, events keep the board fresh.
    let cursor = "";
    do {
      const page = await backlit.records.list("note.", { cursor });
      await Promise.all(page.items.map((it) => refresh(it.key)));
      cursor = page.next;
    } while (cursor);

    // Posting is just a write. The storage.change it triggers — in this tab
    // AND every other open session — is what updates the board, so there's
    // no manual render after the put.
    document.getElementById("post").onclick = () =>
      backlit.records.putJSON(`note.${me.uid}.${Date.now()}`, {
        text: document.getElementById("text").value,
      });

    function render() { /* draw notes; each entry's author is its owner */ }
  })();
</script>
```

Three habits in there are worth naming. **Refetch only the changed key**
— the event names it, so a busy board never re-lists everything. (The
initial seed above still fetches each key on its own; when you want the
whole starting set in one shot instead, reach for
[`records.getBatch`](https://sdk.backlit.run/sdk.md#recordsgetbatchprefix-opts--recordsgetbatchjsontprefix-opts), which returns the
page's values alongside the same cursor.)
**Dedup on the hash** — an event whose `crc32c` matches what you already
hold is a duplicate delivery or your own echo; skip the fetch (the field
semantics are in
[`backlit.on`](https://sdk.backlit.run/sdk.md#backliton--realtime-subscriptions)).
**Let your own write's event drive the UI** — a same-tab write fires
your handler too, so the posting path and the receiving path render
through one code path. And when a view unmounts, call the unsubscribe
function `on` returned (`off()` above) — connection lifecycle and
reconnect behavior are handled for you, per the reference.

## Writing so nobody's edit is lost

The board above dodges write conflicts by design: one note = one key =
one author. The moment several people edit the **same** key — a shared
counter, one list, a settings blob — a plain overwrite silently drops
whichever edit lands second. That's the lost-update bug, and the fix is
always the same helper:
[`updateJSON`](https://sdk.backlit.run/sdk.md#saving-shared-state-safely),
which loads, applies your change, and commits only if nothing moved in
between — retrying against the fresher value when it did.

Realtime raises the stakes on one specific habit: **deferring**. If you
hold a remote change back — say the user is mid-edit in a form — your
in-memory copy is now known-stale, and writing it back with a plain put
would erase the very change you deferred. Write back through
`updateJSON` (or the compare-and-swap primitives under
[`data.putJSONIfMatch`](https://sdk.backlit.run/sdk.md#datagetjson--dataputjson--dataputjsonifmatch))
and the stale copy rebases instead of clobbering. The reference calls
this pattern out explicitly in the delivery-semantics notes under
[`backlit.on`](https://sdk.backlit.run/sdk.md#backliton--realtime-subscriptions).

Also go easy on the wire: every write broadcasts to every connected
session, so a write per keystroke floods your peers (and your usage
allowance). Coalesce rapid edits and save when the value settles — the
debounce guidance lives with the write methods under
[`backlit.data`](https://sdk.backlit.run/sdk.md#backlitdata--glow-shared-keyvalue).

## Who receives what

Events are scoped like the stores they mirror: shared-`data` and
`records` events reach every connected session, `userdata` events reach
only the writing user's own sessions, and `capture` emits nothing — the
per-store rules are under
[`backlit.on`](https://sdk.backlit.run/sdk.md#backliton--realtime-subscriptions).
Remember the read side too: what a visitor can *refetch* on an event
still depends on the glow's auth mode and their sign-in state, per the
[Public, Private, and Accounts matrix](https://sdk.backlit.run/sdk.md#public-private-and-accounts-glows)
— an anonymous visitor on a Public glow can follow live shared-`data`
changes, but records reads need sign-in. Pick the store (and the glow's
auth mode) with the
[permissions guide](https://sdk.backlit.run/guides/permissions-and-write-gating.md)
if that decision isn't settled yet.

## When realtime is the wrong tool

- **A single-user app.** A notepad with one writer has nobody to sync
  with — skip the subscription entirely and just read on load.
- **Reaching people whose tab is closed.** `on` only works while a page
  is open. For "tell me when something happens even if I've left," ship
  a `notify.json` and use push — that whole loop is the
  [push guide](https://sdk.backlit.run/guides/push-end-to-end.md).
- **Correctness that can't miss an event.** Delivery is best-effort;
  if your app breaks when a change slips past it, re-list on a schedule
  or on focus and treat events as an accelerator, per the delivery
  notes under
  [`backlit.on`](https://sdk.backlit.run/sdk.md#backliton--realtime-subscriptions).
