# 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](https://sdk.backlit.run/guides/migrate-a-static-app.md)
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](https://sdk.backlit.run/sdk.md#storing-app-state).
- **It belongs to one signed-in user** (settings, drafts, history) → the
  per-user silo (same
  [storing app state](https://sdk.backlit.run/sdk.md#storing-app-state)
  table).
- **One person posts it, everyone reads it, nobody else edits it**
  (comments, entries, profiles) → the
  [records store](https://sdk.backlit.run/sdk.md#backlitrecords--public-read-owner-write-store).
- **The page captures it but must never read it back** (a contact form,
  telemetry) → the
  [capture store](https://sdk.backlit.run/sdk.md#backlitcapture--write-only-capture-store).

If who-may-write is the part you're unsure about, the
[permissions guide](https://sdk.backlit.run/guides/permissions-and-write-gating.md)
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](https://sdk.backlit.run/sdk.md#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](https://sdk.backlit.run/sdk.md#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](https://glow.backlit.run/openapi.yaml)), 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](https://sdk.backlit.run/sdk.md#public-private-and-accounts-glows)
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](https://sdk.backlit.run/sdk.md#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:

```html
<!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()`](https://sdk.backlit.run/sdk.md#authme-promisebacklituser--null)
(including the `permission` field the form gates on), the first-read
`not_found` default under
[storing app state](https://sdk.backlit.run/sdk.md#storing-app-state),
and `updateJSON` — the default for any key more than one person writes —
under
[saving shared state safely](https://sdk.backlit.run/sdk.md#saving-shared-state-safely).
The error `code`s your handlers can branch on are the
[error-code table](https://sdk.backlit.run/sdk.md#error-codes).

## 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](https://sdk.backlit.run/sdk.md#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](https://sdk.backlit.run/sdk.md#packaging-the-deploy-bundle).

For the ship log, the two docs are honestly this small:

```md
<!-- 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.
```

```md
<!-- 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](https://glow.backlit.run/openapi.yaml) — or skip the
mechanics entirely: an AI assistant connected to the hosted
[MCP server](https://mcp.backlit.run/mcp) 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](https://sdk.backlit.run/sdk.md#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](https://sdk.backlit.run/sdk.md#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](https://sdk.backlit.run/guides/realtime-collaboration.md),
and notify people when something changes even with the tab closed with
the [push guide](https://sdk.backlit.run/guides/push-end-to-end.md).
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 `async` function (see
  [Loading](https://sdk.backlit.run/sdk.md#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](https://sdk.backlit.run/sdk.md#storing-app-state).
- **Shared writes race.** Any key two sessions can write goes through
  `updateJSON`, per
  [saving shared state safely](https://sdk.backlit.run/sdk.md#saving-shared-state-safely).
- **Keys and content types are validated.** Stay inside the charset,
  length, and content-type rules in
  [Validation](https://sdk.backlit.run/sdk.md#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](https://sdk.backlit.run/sdk.md#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 in `records`, not shared `data` — the
  trap is spelled out in the
  [auth-mode matrix](https://sdk.backlit.run/sdk.md#public-private-and-accounts-glows).
