The mirror of 04-migrating-an-app.md. Same evidence, read the other direction:
that guide is a list of things two apps had to undo, and this one is a list of
the same things not done in the first place.
Audience: an agent (or person) scaffolding a new app.
Install the kit first: packages/ui/README.md has the registry line, the token, the
install and pin, the theme import, and the bundler contract. Then start from the
template that matches the app's shape under apps/_templates/ (next,
react-router-ssr for the speedway shape, vite-spa for the walmart shape;
the folder's README says which is which) and follow its TEMPLATE.md for
scaffolding. This doc covers what happens after the app runs and before it has
any screens.
What you get free by starting here#
Worth naming, because each of these is a whole section of the migration guide that does not apply to you.
No parity ledger. There is no accumulated behaviour to preserve, so nothing can be silently purged. This is the single largest cost in a migration and you are exempt from all of it.
No name collisions. Import the kit's StatusDot and it is the only
StatusDot. Keep it that way: the rule that prevents the migration guide's step
1 from ever applying to you is that a local component never takes a kit export's
name. If you need a variant, name it for what makes it different.
A pin you chose. Take the current kit version deliberately, and know that a
caret on a 0.0.x version is a patch pin, so you are choosing an exact version
whether you meant to or not. Every canon rule is reachable at the latest; that
stops being true the day you stop bumping.
Three decisions to make before the first screen#
None of these are reversible cheaply, and all three are decided by accident if nobody decides them.
Where filter and detail state lives. URL or component. The canon says URL
params for a browsable list's filters (07 §A3, AP-10 row 15) and the two
existing apps both do it by hand: speedway wires useSearchParams directly,
walmart wrote a local useQSync hook. The kit's sync seam exists and works, and
ships only a no-op adapter, so writing a real one is a genuine option that would
also make the atomic-write rule structural instead of remembered. Pick one of the
three, and write it down.
Whether you adopt ?hl=. The convention of returning from a create back to
its list without losing position. Speedway owns the ?hl= name; walmart drives
the same behaviour under ?job=. The canon calls ?hl= app-wide and it is not,
which is a live inconsistency rather than settled law. Decide, and if you adopt
it, say so somewhere a reader will find.
Which toolbar. The kit now ships FacetBar, the union of both product
toolbars per the owner's 2026-08-14 ruling; its contract lives in
packages/ui/docs/table.md. A greenfield app reaches for it first and becomes
its first product caller; know that you are the proving ground. The older kit
FilterBar and the two divergent app-local toolbars remain in the tree; the
call-site ledger for all of them lives in 12-primitives-and-rules.md, cite
it rather than restating counts here.
Build order#
The same outward-in order as a migration, for the same reason: a page built inside an unsettled shell has two layout owners.
- Shell.
AppShell,Sidebar,Topbar. - Page composition.
Card,PageTitle, and the one-owner-of-inset rule (05 §A4, and reach forDataTable'sbleedrather than hand-rolling negative margins). - States, before any real page needs them.
PageInfo,EmptyState,PageLoading,PageError, skeletons. Build the three-way branch once, truly empty against filtered-to-nothing against still-loading, and every later surface inherits it. - The first real screen, from a recipe rather than from scratch.
- The app's own completeness, before anyone reviews a screen: a favicon
(
src/app/icon.svg), a title per route (metadataon everypage.tsx, the app's name as the layout's title template), anot-found.tsxand anerror.tsxin the kit's page-state idiom, the theme boot, andGET /build-info. None of these is a design decision and every one of them has been called out in an owner review after the fact (docs/GUIDEBOOK.mdlapse row 10: no favicon or page titles at a release). The template carries them, andnpm run doctor(scripts/app-doctor.mjs) names any that are missing with the fix, so an agent building the tenth screen is pushed to finish the app rather than reminded by this paragraph. Run it before the first review round and in CI. Its siblingnpm run canon-check(the kit's bin) holds the canon feedback seam: a change touching UI paths adds a dated line toCANON-NOTES.mdor carries a human overrule in the PR body, and the added lines are read for the deviation classes the retrospective names. The template ships both; keep both in CI (docs/plan/71-canon-feedback-loop-plan.md).
Build screens from the recipes, not from the component list#
The component docs answer "what props does this take". They do not answer "what does this screen need", and the gap between those two questions is where incompleteness comes from.
- Asked for a list, table, queue, or index:
10-recipe-browsable-list.md. It is 27 rows of things that request brings along whether or not anyone said them. - Asked for a detail, preview, or drill-in:
11-recipe-record-detail.md. Answer its row zero, peek against modal against page, before writing anything.
Both recipes end in a do-not list mined from corrections on shipped code. Every entry is something that was built, shipped, and caught by a human looking at the screen. Reading those is the cheapest hour available to a new app.
Keep the canon reachable#
Two habits that cost nothing now and a lot later.
Bump the kit deliberately and regularly. The gap between what the canon
documents and what your pin can reach is invisible until someone follows a rule
that does not work. Speedway is the cautionary case: it declares ^0.0.14
against a kit at 0.0.25, and its dev tree hides that behind a symlink, so the
divergence only appears in a clean install.
When you deviate, record why in your own surface's doc. Owner ruling D1 says a surface may break the canon's overlay tree when the break buys better and more consistent UX, and that the reason belongs in that surface's doc. Deviation is legitimate and never free. What the ruling retired was silent discretion, not discretion.
When you hit a rule the canon does not have, that is the canon's bug. Of 73
mined corrections across the two existing apps, zero were retrieval failures:
every gap was law that had never been written down, not law someone failed to
find. A third app will find more, and the docs only get cheaper for the fourth if
what you learn goes back into docs/design-language/ rather than staying in your
repo.