Build a new app on Backlit
You're starting from a blank page: an idea, no code yet. Shipping it as a
glow means writing ordinary static HTML against the window.backlit SDK,
wrapping it in a small deploy bundle, and promoting a previewed draft to
live. This guide is the end-to-end sequence for that first ship — the
per-method facts live in the reference, and this guide links to them
rather than restating them.
(Porting an app that already exists — one that keeps state in
localStorage or indexedDB? Start from the
migration guide
instead; it covers the rewrite this guide gets to skip.)
Step 1 — sketch the data model before the UI
Backlit gives you three general stores plus a write-only inbox, and the whole design question is who owns each piece of state. Before writing any markup, list what the app remembers and sort each item:
- Everyone sees it, privileged users edit it (a board, a roster, a published doc) → the glow-shared store, per storing app state.
- It belongs to one signed-in user (settings, drafts, history) → the per-user silo (same storing app state table).
- One person posts it, everyone reads it, nobody else edits it (comments, entries, profiles) → the records store.
- The page captures it but must never read it back (a contact form, telemetry) → the capture store.
If who-may-write is the part you're unsure about, the permissions guide is the recipe for exactly that decision.
Then choose keys. The keyspace is flat — no slashes, no folders — so
compose hierarchy into the key itself (note.{id}, prefs), staying
inside the charset and length rules in
Validation. Prefer one key
per independent record over one giant blob: unrelated edits then never
touch the same key, which matters the moment two people write at once
(see saving shared state safely).
Write the resulting table down — it becomes your DATA.md in Step 4.
Step 2 — pick who can reach the app
A glow's auth mode decides whether visitors arrive anonymous, must
sign in, or can sign themselves up. It's a property of the glow (set at
create, changeable later via the
management API), not of your
code — but it shapes your code: it decides whether auth.me() can
return null and which stores each visitor can use. The
Public, Private, and Accounts matrix
is the authority; as a rule of thumb, a team tool wants Private (only
allowlisted people get in, your code never sees a signed-out state), a
community app wants Accounts (anyone can join — but a self-serve account
starts as a viewer, which can post to records and not to shared
data), and an open read-mostly page wants Public.
Step 3 — write the app against the SDK
Load the SDK with one script tag and use window.backlit.* — there is
no build step, no bundler, no npm install. The tag, the URL channels,
and the async ground rules are in
Loading; note that the SDK
only talks to its backend on a deployed glow host, so the data surface
comes alive when you deploy (Step 5), not in a local preview.
Here's a complete, honest first app — a team "ship log" for a Private glow. It reads shared state with a first-run default, gates the form on the visitor's permission, and writes with the safe shared-write helper:
<!doctype html>
<meta charset="utf-8">
<title>Ship Log</title>
<ul id="log"></ul>
<form id="add" hidden><input id="entry" placeholder="What shipped?"><button>Add</button></form>
<script src="https://sdk.backlit.run/sdk/v1/sdk.js"></script>
<script>
(async () => {
const me = await backlit.auth.me();
const form = document.getElementById("add");
// Viewers can read but not write shared data — reflect that in the UI.
form.hidden = !me || me.permission === "viewer";
let log = { entries: [] };
try {
log = (await backlit.data.getJSON("log")).value;
} catch (e) {
if (e.code !== "not_found") throw e; // first run: keep the default
}
render(log);
form.onsubmit = async (ev) => {
ev.preventDefault();
const text = document.getElementById("entry").value.trim();
if (!text) return;
// A shared key several people write → updateJSON, never bare putJSON.
const { value } = await backlit.data.updateJSON("log", (cur) => {
cur ??= { entries: [] };
cur.entries.push({ text, by: me.name || me.email, at: Date.now() });
return cur;
});
render(value);
form.reset();
};
function render(l) {
document.getElementById("log").replaceChildren(...l.entries.map((e) => {
const li = document.createElement("li");
li.textContent = `${e.text} — ${e.by}`; // textContent: user strings are not HTML
return li;
}));
}
})();
</script>
The moves it demonstrates are each documented once in the reference:
branching on
auth.me()
(including the permission field the form gates on), the first-read
not_found default under
storing app state,
and updateJSON — the default for any key more than one person writes —
under
saving shared state safely.
The error codes your handlers can branch on are the
error-code table.
Step 4 — wrap it in a deploy bundle
A glow deploys as an archive with three required files at its root:
index.html plus two short docs, APP.md (what the app is, who it's
for) and DATA.md (which keys live in which store — the table you wrote
in Step 1). The exact contract, the reject behavior for a missing file,
and the fact that both docs are served publicly are the
deploy bundle contract;
the archive format, the browser-storage scan (a bundle that references
localStorage/indexedDB is rejected), and the packing command are
under
packaging the deploy bundle.
For the ship log, the two docs are honestly this small:
<!-- APP.md -->
# Ship Log
A shared changelog for the team: anyone on the allowlist can read it,
editors add a line when something ships. The team lead administers it.
<!-- DATA.md -->
# Data layout
| Namespace | Key | Meaning |
| --- | --- | --- |
| `data` | `log` | the shared list of shipped entries (JSON, written via `updateJSON`) |
Step 5 — deploy a draft, preview, then promote
Don't flip your first upload straight to live. Stage it as a draft, open the preview URL, click through the real flows (sign in, a write, a reload), and only then promote. The draft/promote endpoints are in the deploy API — or skip the mechanics entirely: an AI assistant connected to the hosted MCP server can create the glow, deploy the bundle as a draft, hand you the preview URL, and promote on your go-ahead. Deploy with an agent has the connection details and the two out-of-band transports.
Step 6 — then outgrow the static page
Once the first version is live, the upgrades a plain static page could never do are one step away, and the reference keeps a menu of them — offer advanced features that fit the app. The two with their own guides: make shared state update live across every open tab with the realtime guide, and notify people when something changes even with the tab closed with the push guide. Add what fits the app; skip what doesn't.
Gotchas to plan for
- Every read and write is async. There is no synchronous storage;
structure init as an
asyncfunction (see Loading for why the IIFE). - First read of any key throws
not_found. Catch it and start from a default — the pattern under storing app state. - Shared writes race. Any key two sessions can write goes through
updateJSON, per saving shared state safely. - Keys and content types are validated. Stay inside the charset, length, and content-type rules in Validation — the SDK rejects violations before they leave the page.
- No browser storage in the bundle. The deploy scan rejects it —
the rule is in
packaging the deploy bundle.
Design on
window.backlit.*from the first line and it never bites. - An Accounts sign-up is a
viewer. Design self-serve apps so user-generated content lands inrecords, not shareddata— the trap is spelled out in the auth-mode matrix.