Migrate chrome-light app surfaces to SvelteKit; docs stay on Astro/Starlight (two apps, one origin)

0043-sveltekit-app-astro-docs-split

decision read as Explain confidence verified status active 2026-06-16 owner forward-deployed-engineer
Reversibility
two-way door

DEC-0043 — Migrate chrome-light app surfaces to SvelteKit; docs stay on Astro/Starlight

Reversibility: two-way door — the rewrite + apex-domain assignment is trivially undoable (move the domain back to the docs project, delete the /knowledge rewrite; a subdomain split is kept as a fallback). The phased plan moves the apex domain only ONCE, at the final cutover, after the full SvelteKit app is proven on a preview URL — no big-bang. The durable commitments are (a) SvelteKit for the app surfaces and (b) the @dossier/okf-view extraction (good regardless of framework). Starlight is not abandoned — it remains the docs renderer.

Decided by the user this turn; topology scoped by the Principal Platform Architect and repo-verified. Recorded faithfully — the why below is the real trigger and reasoning, not reconstructed. Built, shipped, and verified live on production (2026-06-17): confidence: verified — see Review for the gate evidence. (Recorded asserted when decided 2026-06-16; the promotion to verified and the corrected unification mechanism are captured in Review.)

Context

The interactive surfaces (/, /board, /graph, the client-preview page, and /api/subscribe) and the docs surface (/knowledge/*) currently all live in one Astro project, @dossier/site (packages/site), built across Astro Starlight as the docs-site generator + the product-owner, starlight-engineer, and documentation-engineer functions (Starlight as the renderer), Recalibrate the Dossier brand identity — demote color, promote type + restraint + craft — then build the showcase landing (the landing), Agentic "sprint board" architecture — a git-resident OKF task board worked by bounded, hook-governed Agent SDK loops / the board build (/board, /graph), and Site chrome unified into shared SiteHeader + SiteFooter + ThemeToggle components, and the docs/KB surface made fully open — DEC-0022's DOCS_ENABLED landing-only gate is removed so /knowledge ships publicly alongside the landing/board/graph (shared chrome, fully-open KB). All CSS is --ds-* token-based from Establish the design system and the UX-engineering function.

The real trigger is a recurring, hard-to-kill Astro <ClientRouter/> view-transition flash on navigation. It was investigated live on production via animationstart capture: the UA root crossfade fired on every nav. Author animation: none !important on the root view-transition pseudos (both the named (root) group and the universal (*)) did not override the Chromium UA animations on this site (verified across multiple deploys). Stripping the root's view-transition-name reduced it from 5 animations to a single residual new-page fade-in but did not fully eliminate it. Two prior "fix" commits (777782e, and the first pass of the animation: none approach) shipped but never actually suppressed the crossfade.

Rather than keep fighting Astro's view transitions, the user chose to move the interactive surfaces to a real app framework. SvelteKit's client router does plain client-side navigation with no forced view transitions, so the entire bug class disappears and the astro:after-swap / .navigated no-FOUC-survival machinery is deleted (net simplification). SvelteKit is Vite-based, so this stays on the existing VoidZero/Vite TypeScript stack (CLAUDE.md tech direction held).

Repo-verified topology facts (from the architect)

  • @dossier/design (CSS custom-property tokens, Establish the design system and the UX-engineering function) and @dossier/okf (schema/graph; deps yaml+zod only) are framework-agnostic and consumed by both apps unchanged.
  • Data loading in SvelteKit: build-time +page.server.ts load() calling @dossier/okf + @dossier/okf-view directly with prerender = true — the same static output profile as Astro's getCollection today (the content collection was always a thin layer over a filesystem reader we own — packages/site/src/lib/okf-routes.mjs).
  • The d3 graph island (graph-island.ts, reads its payload from a #graph-data script, idempotent mountGraph(host)) ports to a Svelte onMount with zero rewrite.
  • Board interactivity (~270 lines vanilla JS) is kept in onMount for the migration and Svelte-ified as a fast-follow (explicitly deferred, not dropped).
  • Theme continuity: KEEP the starlight-theme localStorage key (never rename — Starlight owns it); the pre-paint no-FOUC script moves to SvelteKit's app.html head; cross-app continuity is automatic because both apps read the same key on the same origin under the rewrite.

Options considered

  1. Stay on Astro, keep fighting the view-transition flash — drop <ClientRouter/> for an MPA, or pile on more CSS overrides. The lower-effort path, but rejected by the user in favor of a real app framework: the author overrides have repeatedly failed to beat the Chromium UA animations on this site, so the fix is not reliably in reach.
  2. Single Vercel project where SvelteKit serves Starlight's built static output under /knowledge. Rejected: brittle asset-hashing / base-path conflicts, and it couples the two surfaces' release cadences.
  3. Two Vercel projects + a path rewrite from the SvelteKit primary (CHOSEN). Works on Vercel, is documented (multi-zone via rewrites), and is a two-way door. A subdomain split is kept as a fallback, but /knowledge is a same-origin path today and the user did not ask to change that.

Decision

Split the surface into two apps on one origin:

  • App surfaces → a NEW SvelteKit app (recommended package name @dossier/app): the landing (/), the board (/board), the graph (/graph), the client-preview page, and the /api/subscribe endpoint. SvelteKit's plain client-side nav removes the view-transition flash entirely; the astro:after-swap no-FOUC machinery is deleted.
  • Docs surface → STAYS on Astro + Starlight (recommended rename @dossier/site@dossier/docs), stripped down to docs-only (/knowledge/*). Starlight is retained as the docs renderer at the user's explicit direction — this is the scoped part of the supersession (see below).
  • One origin via Vercel: the SvelteKit app is the primary project and owns the apex domain; a vercel.json path rewrite proxies /knowledge/:path* to the separate docs project's production alias (Vercel's documented multi-zone-via-rewrites pattern).
  • Shared leaf package @dossier/okf-view extracted from packages/site/src/lib/okf.ts (the board view-model) + okf-routes.mjs (the filesystem KB reader + route map + Starlight sidebar), so both apps read the KB from one source of truth rather than forking it — the atomic / single-source-of-truth bar applied to the surface layer.

Scoped supersession (recorded precisely)

This partially supersedes the IMPLICATION of Establish the design system and the UX-engineering function and Astro Starlight as the docs-site generator + the product-owner, starlight-engineer, and documentation-engineer functions — that Astro/Starlight is the renderer for all surfaces — FOR THE APP SURFACES ONLY. Starlight is not abandoned: it remains the docs renderer (DEC-0015's core mandate stands for /knowledge/*). DEC-0010's tokens-first design language is untouched and consumed by both apps. The frontmatter carries no supersedes edge to DEC-0010/DEC-0015 because neither decision is superseded as a whole — only the all-surfaces-on-Astro implication is narrowed; this is captured as relates_to + this prose, deliberately. The Recalibrate the Dossier brand identity — demote color, promote type + restraint + craft — then build the showcase landing entrance choreography ports to Svelte component-mount (naturally play-once), and the shared chrome from Site chrome unified into shared SiteHeader + SiteFooter + ThemeToggle components, and the docs/KB surface made fully open — DEC-0022's DOCS_ENABLED landing-only gate is removed so /knowledge ships publicly alongside the landing/board/graph re-homes into Svelte components in the app (with @dossier/okf-view feeding the nav/route data both apps need).

Pending naming detail

Package names @dossier/app (SvelteKit) + @dossier/docs (renamed Astro) are the recommendation and are pending user confirmation. The decision does not block on the names — the topology stands regardless of the final identifiers.

Rationale

  • Kill the bug class, not the symptom. Two prior fixes shipped without suppressing the Chromium UA crossfade; SvelteKit's router has no forced view transitions, so the flash cannot occur and the survival machinery is deleted rather than patched.
  • Stay on the stack. SvelteKit is Vite-based — the VoidZero/Vite TypeScript lean (CLAUDE.md) is preserved; no new build paradigm is introduced.
  • Single source of truth for the KB read. Extracting @dossier/okf-view means the board view-model and the filesystem KB reader / route map / sidebar are authored once and consumed by both apps — never forked (the atomic SSOT bar, Dossier — The Knowledge Model (v0)).
  • Sovereignty preserved. Both apps are derived, replaceable surfaces over the OKF git repo (Adopt OKF as Dossier's canonical knowledge format); the static-prerender data-loading profile is unchanged (filesystem reader → @dossier/okf → prerendered pages).
  • Keep what works. The d3 graph island and the board's vanilla JS carry forward un-rewritten (the latter Svelte-ified as a fast-follow); the starlight-theme key and the pre-paint no-FOUC script are retained, so theme continuity is automatic on the shared origin.
  • Two-way door by construction. The domain moves exactly once, at the final cutover, after the full app is proven on a preview URL — no big-bang; the rewrite and domain assignment are trivially reversible.

Consequences

  • Two deploy projects instead of one — bought back by independent app/docs release cadences (the coupling option #2 was rejected to avoid).
  • One extra edge hop for /knowledge (the rewrite proxy) — negligible.
  • Cross-app navigation is a full navigation (app ↔ docs) — intended, not a regression.
  • Board vanilla-JS interactions carried forward un-Svelte-ified until the fast-follow (deferred work, tracked — see Review / the filed task).
  • Effort: ~4–6 focused days, dominated by the 488-line landing port, which must port pixel-faithfully per the landing content model's invariants (Recalibrate the Dossier brand identity — demote color, promote type + restraint + craft — then build the showcase landing).
  • Nothing in product capability is lost. All CSS is --ds-* token-based (Establish the design system and the UX-engineering function) and moves verbatim.
  • DEC-0010/DEC-0015 narrowed, not reversed — Starlight remains the docs renderer; the design language is unchanged.

Review

Promoted asserted → verified (2026-06-17). The migration is built, shipped, and confirmed live on production by the Principal Forward Deployed Engineer and the user. The promotion gate below is met.

The gate as originally stated (when recorded asserted): the SvelteKit app stands up all surfaces on a preview URL, and the cutover serves every route — /, /board, /graph, /knowledge/*, /api/subscribe — on the real apex domain with (a) theme continuity intact across the app↔docs boundary (shared starlight-theme key, pre-paint no-FOUC script in app.html) and (b) zero nav flash (the original trigger gone).

Gate evidence on production — both Vercel projects are deployed and the unified origin works end-to-end:

  • App (@dossier/app, SvelteKit): production https://dak-dossier.vercel.app. Docs (@dossier/docs, Astro/Starlight): https://dak-dossier-docs.vercel.app (the rewrite target).
  • Live curl of the app origin: /, /board, /graph200 served by the APP (board renders board-toolbar + ds-task-card); /api/subscribe200 app function ({"ok":true,"service":"subscribe"}); /knowledge/, /decisions/0043-…/, /tasks/…/200 proxied to the DOCS (Starlight sidebar, OKF ds-badge--decision meta, typed-edge "Related" nav); the docs' root-level hashed assets (/_astro/…) resolve via the app origin → docs are styled under the proxy.
  • (a) Theme continuity holds across the app↔docs boundary — the kept starlight-theme key + the app.html pre-paint no-FOUC script; both themes flip correctly on one origin.
  • (b) Zero nav flash — the DEC-0043 trigger (Astro <ClientRouter/> view-transition crossfade) was proven gone in-browser: startViewTransition called 0 times on client nav.

No custom apex domain — production is the Vercel project domain. The user runs on the Vercel-assigned production domain dak-dossier.vercel.app; there is no separate custom apex/domain attached. The gate's "served on the real apex domain" clause is satisfied by the project's production domain here — there is simply no separate custom domain to move. If a custom domain is attached later it is a trivial domain-attach to the app project, no re-architecture.

Unification mechanism — corrected from the original plan. This decision (and the early vercel.json) planned a /knowledge/:path* rewrite to the docs project. That rewrite was insufficient and was corrected (commit 61b74d0 on main): KB atoms render at top-level routes (/decisions/*, /tasks/*, /roles/*, /model/*, /references/*, /log/, /mission/) — not under /knowledge/ — and Starlight emits root-level hashed assets (/_astro/*, /print.*.css, /page.*.js, /Search.*.js), so a /knowledge-only rewrite 404'd every top-level atom deep-link and left the docs unstyled. The shipped unifier in packages/app/vercel.json is a guarded catch-all rewrite: it proxies every path to the docs project except the app's own namespace (/, board, graph, preview, api, _app, favicon — source: "/:path((?!api/|preview|board|graph|_app/|favicon\\.svg).+)"). Vercel applies vercel.json rewrites as a fallback (after the app's own static files + functions), so the app's routes win and only docs paths fall through. The phased plan was driven by the Principal Forward Deployed Engineer; the migration is tracked + closed at Execute the SvelteKit app migration (DEC-0043) — phased, no big-bang, apex domain moved only at final cutover. Fast-follows remain open: board Svelte-ification (Svelte-ify the board's ~270 lines of vanilla-JS interactivity (the DEC-0043 / Phase-7 fast-follow)), email-template re-home (Re-home the Buttondown email templates from packages/site to @dossier/app (the subscribe endpoint moved)), docs @vercel/analytics decision (Decide whether @dossier/docs keeps @vercel/analytics now that the landing (its only consumer) moved to @dossier/app), and the landing-path drift (Fix the stale round-trip-test path in the generate-landing SKILL.md (points at a file that no longer exists)).