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 (event shape, which sessions receive which events, delivery guarantees, reconnects) and the safe-write loop under saving shared state safely. This guide links to both rather than repeating them, so it can't drift out of sync with them.

Plain text All guides SDK reference

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). 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. 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 under one key per note, and each event carries the note's owner.

<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, 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). 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, 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) and the stale copy rebases instead of clobbering. The reference calls this pattern out explicitly in the delivery-semantics notes under backlit.on.

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.

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. 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 — 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 if that decision isn't settled yet.

When realtime is the wrong tool