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.
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:
- Seed — read or list the state you display.
- Subscribe — on a matching event, refetch that key and re-render.
- 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
- 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.
ononly works while a page is open. For "tell me when something happens even if I've left," ship anotify.jsonand use push — that whole loop is the push guide. - 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.