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
is the from-scratch counterpart of this sequence.)
The one table you'll lean on hardest is the Migrations mapping — 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.
- This signed-in user's own (settings, drafts, history) → the per-user silo (also covered under storing app state).
- Owned content others read but can't edit (posts, comments, profiles) → the owner-write records store.
The Migrations mapping 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 section:
- Storage is async. A synchronous
getItembecomes an awaited call, so the work that depended on it moves into anasyncinitializer. - Objects go through the JSON helpers. Instead of hand-rolling
JSON.stringify/parse, use the*JSONhelpers described under 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 instead of a plain put. The reference's 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; 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 — 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; 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 and 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, and the upload endpoints themselves are in the deploy API.
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.
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 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 in the reference for the connection details.
Gotchas to plan for
- Async everywhere. Anything that read synchronously from
localStoragenow 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.
- Key charset. Slash-y or exotic keys must be flattened — see Migrations.
- Sign-in requirements differ by surface. Some stores need a signed-in user; the Migrations mapping 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.