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.)

Plain text All guides SDK reference

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:

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