# Migrate a static app to Backlit

You have a standalone HTML app that keeps its state in `localStorage`,
`sessionStorage`, or `indexedDB`. Porting it to a glow means three
things: swap each browser-local store for the Backlit surface that
matches *who the data belongs to*, satisfy the deploy bundle contract,
and ship it as a draft you promote once it looks right. This guide is the
end-to-end sequence; the per-method facts live in the reference and this
guide links to them. (Starting from a blank page instead of porting? The
[build-a-new-app guide](https://sdk.backlit.run/guides/build-a-new-app.md)
is the from-scratch counterpart of this sequence.)

The one table you'll lean on hardest is the
[Migrations mapping](https://sdk.backlit.run/sdk.md#migrations) — browser
API on the left, Backlit surface on the right. Read it there; this guide
narrates the workflow *around* it rather than reprinting it.

## Step 1 — decide who each piece of state belongs to

Before touching code, sort every `localStorage`/`indexedDB` key into one
of three buckets. Ownership is the whole decision:

- **Global, everyone sees it** (a leaderboard, a published page, a shared
  board) → the glow-shared store,
  [storing app state](https://sdk.backlit.run/sdk.md#storing-app-state).
- **This signed-in user's own** (settings, drafts, history) → the
  per-user silo (also covered under
  [storing app state](https://sdk.backlit.run/sdk.md#storing-app-state)).
- **Owned content others read but can't edit** (posts, comments,
  profiles) → the owner-write records store.

The [Migrations mapping](https://sdk.backlit.run/sdk.md#migrations) spells
out exactly which browser API maps to which surface and when to pick each
— including where write-only submissions (contact forms, telemetry) go.
Don't restate it in your head; open it and place each key.

## Step 2 — port the reads and writes

Two shifts change how the code is shaped, both documented in the
[Migrations](https://sdk.backlit.run/sdk.md#migrations) section:

- **Storage is async.** A synchronous `getItem` becomes an awaited call,
  so the work that depended on it moves into an `async` initializer.
- **Objects go through the JSON helpers.** Instead of hand-rolling
  `JSON.stringify`/`parse`, use the `*JSON` helpers described under
  [storing app state](https://sdk.backlit.run/sdk.md#storing-app-state).

There's one correctness rule the port must respect, and it's the single
most common migration bug: a naive load → mutate → save is safe for
per-user data (you're the only writer) but a lost-update hazard on
**shared** data that more than one session can write. For any shared key,
use the read-modify-write helper from
[saving shared state safely](https://sdk.backlit.run/sdk.md#saving-shared-state-safely)
instead of a plain put. The reference's
[Migrations](https://sdk.backlit.run/sdk.md#migrations) example shows the
before/after shape; follow it.

A note on **keys**: browser storage lets you use any string, but a glow's
keyspace is restricted. If your old keys contain slashes or other
disallowed characters, rewrite them to a flat scheme — the exact rule is
in [Migrations](https://sdk.backlit.run/sdk.md#migrations); translate,
don't guess.

## Step 3 — proactively offer what a static page couldn't do

A faithful port reproduces the original behavior, but moving onto Backlit
unlocks capabilities a plain static page never had — realtime sync,
owner-scoped content, per-user silos, a private submission inbox,
permission-aware controls. If you're an AI agent doing the migration,
read what the app does and *offer* the ones that genuinely fit before you
deploy. The menu of "if the app does X, offer Y" is
[Offer advanced features that fit the app](https://sdk.backlit.run/sdk.md#offer-advanced-features-that-fit-the-app)
— suggest only what suits the app, and wire it in if the user says yes.

## Step 4 — satisfy the deploy bundle contract

A glow can't be *just* your HTML. The bundle has required root files, and
one hard prohibition that a ported static app frequently violates:

- **Required root files.** The bundle root must carry the app entry plus
  two short docs describing what the app is and how it uses the data
  namespaces. The exact filenames and what each must contain are the
  [deploy bundle contract](https://sdk.backlit.run/sdk.md#deploy-bundle-contract);
  add them if your original didn't have them.
- **No browser-local storage in the shipped code.** This is the whole
  point of Step 2 — a bundle that still reaches for browser-local
  persistence is rejected at deploy time. The deploy pipeline scans for
  it; the [deploy bundle contract](https://sdk.backlit.run/sdk.md#deploy-bundle-contract)
  and [packaging the deploy bundle](https://sdk.backlit.run/sdk.md#packaging-the-deploy-bundle)
  are the authority on what's scanned and rejected, so finish the port
  before you package.

How the bundle is assembled (the archive format, the upload request) is in
[packaging the deploy bundle](https://sdk.backlit.run/sdk.md#packaging-the-deploy-bundle),
and the upload endpoints themselves are in the
[deploy API](https://glow.backlit.run/openapi.yaml).

## Step 5 — deploy a draft, preview, then promote

Don't flip a migrated app straight to live. Ship the bundle as a
**draft** first, open the draft URL, click through the ported flows
(sign-in, a write, a reload to confirm state survives), and only then
promote the draft to live. The draft/promote endpoints are in the
[deploy API](https://glow.backlit.run/openapi.yaml).

## Let an agent do the mechanical parts

You don't have to package and upload by hand. An AI assistant connected
to the hosted [MCP server](https://mcp.backlit.run/mcp) can create the
glow, deploy the ported bundle as a draft, and promote it live once
you've previewed it — the same draft-first sequence as Step 5, driven for
you. See
[Deploy with an agent](https://sdk.backlit.run/sdk.md#deploy-with-an-agent)
in the reference for the connection details.

## Gotchas to plan for

- **Async everywhere.** Anything that read synchronously from
  `localStorage` now awaits; audit your init path for hidden synchronous
  assumptions.
- **The shared-write race.** Plain put on a shared key that two sessions
  can touch loses updates. Use the safe helper from
  [saving shared state safely](https://sdk.backlit.run/sdk.md#saving-shared-state-safely).
- **Key charset.** Slash-y or exotic keys must be flattened — see
  [Migrations](https://sdk.backlit.run/sdk.md#migrations).
- **Sign-in requirements differ by surface.** Some stores need a
  signed-in user; the [Migrations mapping](https://sdk.backlit.run/sdk.md#migrations)
  notes which, and the store's own reference section has the precise gate.
- **The bundle scan is at deploy time, not runtime.** A leftover
  browser-storage call fails the deploy, not the page — finish Step 2
  before Step 4.
