Plan: Migrate Frontend from React to Svelte
Context
The apps/web/ directory is a React 19 + React Router v6 SPA bundled with esbuild via Nx. The goal is to replace it entirely with a Svelte 5 + SvelteKit (SPA mode) application while preserving all existing behavior — the same routes, the same WebSocket/document-model integration, the same Tailwind styling, and the same backend API.
Three pure-JS services (EventManager.ts, documentModelStore.ts, api.ts) have no React dependencies and can be reused as-is. Everything else — 43 components, hooks, contexts, and registries — needs to be rewritten in Svelte.
Technology Choices
| Concern |
React (current) |
Svelte (target) |
| Framework |
React 19 |
Svelte 5 |
| Routing |
React Router v6 |
SvelteKit (SPA mode via @sveltejs/adapter-static) |
| Build |
@nx/esbuild:esbuild |
@sveltejs/kit + Vite |
| State (global) |
React Context |
Svelte writable stores |
| State (external) |
useSyncExternalStore |
Native $store / subscribe protocol |
| Lazy components |
React.lazy + Suspense |
Dynamic import() + {#await} |
| UI primitives |
Radix UI |
Bits UI (bits-ui) — headless Svelte components |
| Styling |
Tailwind + CVA + clsx |
Same (unchanged) |
| Icons |
lucide-react |
lucide-svelte |
Why SvelteKit SPA mode? It is the standard Svelte app scaffold (Vite-based, active ecosystem) and its file-based routing cleanly replaces React Router. Disabling SSR with adapter-static + prerender = false gives a pure SPA — identical to the current app.
Phase 1 — Scaffold the Svelte App
1a. Replace apps/web/ with SvelteKit
- Rename
apps/web/ → apps/web-react/ (keep as reference during migration)
- Scaffold new
apps/web/ with npx sv create selecting:
- SvelteKit (Vite)
- TypeScript
- Tailwind CSS
- No SSR (
adapter-static, prerender = false)
- Update
apps/web/project.json (Nx targets) to delegate to vite build / vite dev
- Copy path aliases from old
tsconfig.app.json:
@/* → apps/web/src/*
@multiplayer-base/shared-types → libs/shared-types/src/index.ts
- Copy
tailwind.config.js and postcss.config.js from old app
1b. Install new dependencies
bits-ui # headless Svelte component primitives (replaces Radix UI)
lucide-svelte # icon library
class-variance-authority # keep as-is
clsx + tailwind-merge # keep as-is
axios # keep as-is
Remove: react, react-dom, react-router-dom, all @radix-ui/*, lucide-react
Phase 2 — Migrate Pure-JS Services (no changes needed)
Copy these files verbatim into apps/web/src/lib/services/:
EventManager.ts — WebSocket singleton
documentModelStore.ts — External store (already uses a subscribe interface compatible with Svelte stores)
api.ts — Axios HTTP client
Phase 3 — Auth Store (replaces AuthContext)
New file: apps/web/src/lib/stores/auth.ts
Replace AuthContext.tsx with a plain Svelte writable store. Components read with $authStore.user instead of useAuth(). PrivateRoute becomes a SvelteKit layout guard in +layout.svelte.
Phase 4 — Routing (SvelteKit file-based)
src/routes/
├── +layout.svelte
├── +page.svelte # redirect to /dashboard
├── login/+page.svelte
├── register/+page.svelte
├── auth/callback/+page.svelte
└── dashboard/
├── +layout.svelte # PrivateRoute guard
├── +page.svelte
├── user/+page.svelte
├── author/+page.svelte
├── admin/+page.svelte
└── settings/+page.svelte
Phase 5 — Core Components
LayoutDocumentView.svelte — replace useSyncExternalStore with Svelte subscription + onMount/onDestroy
LayoutRenderer.svelte — replace React.lazy+Suspense with dynamic import()+{#await}; pure-JS utilities copy unchanged
layoutRegistry.ts — replace React.lazy() with plain () => import() returning a Promise
TargetPortal — custom Svelte portal via document.getElementById + tick() or Svelte 5 mount()
PageHeader.svelte — straightforward rewrite
Phase 6 — UI Components (Radix → Bits UI)
| Old (Radix) |
New (Bits UI) |
accordion.tsx |
bits-ui Accordion.* |
tabs.tsx |
bits-ui Tabs.* |
button.tsx |
bits-ui Button or custom |
tree-view.tsx |
Rewrite with Bits Accordion |
Phase 7 — Layout Components
10 layout components rewritten as .svelte files. Key translations:
useRef scroll → bind:this + afterUpdate
bind:value replaces controlled inputs
useState + Context → writable store via Svelte context
useMemo → $: derived statements
Phase 8 — Pages
7 pages rewritten as SvelteKit +page.svelte files using goto() for navigation and $page.url.searchParams for query params.
Phase 9 — Message Components
6 message components rewritten as Svelte components using the Svelte portal solution.
Phase 10 — Registries
Update lazy import syntax in layoutRegistry.ts, messageRegistry.ts, documentRegistry.ts.
Files NOT changed
apps/api/ — untouched
libs/shared-types/ — untouched
apps/api-e2e/ — untouched
Verification
npm run restart:web — dev server starts on :4200
- Login/register flows work
- Google OAuth completes end-to-end
- User dashboard loads with layout from workflow JSON
- Chat sends and receives messages via WebSocket
- Admin page lists users, role update works
- Settings page saves changes
npx nx test api — backend tests still pass
- Manual smoke test of all 8 routes
Plan: Migrate Frontend from React to Svelte
Context
The
apps/web/directory is a React 19 + React Router v6 SPA bundled with esbuild via Nx. The goal is to replace it entirely with a Svelte 5 + SvelteKit (SPA mode) application while preserving all existing behavior — the same routes, the same WebSocket/document-model integration, the same Tailwind styling, and the same backend API.Three pure-JS services (
EventManager.ts,documentModelStore.ts,api.ts) have no React dependencies and can be reused as-is. Everything else — 43 components, hooks, contexts, and registries — needs to be rewritten in Svelte.Technology Choices
@sveltejs/adapter-static)@nx/esbuild:esbuild@sveltejs/kit+ VitewritablestoresuseSyncExternalStore$store/subscribeprotocolReact.lazy+Suspenseimport()+{#await}bits-ui) — headless Svelte componentslucide-reactlucide-svelteWhy SvelteKit SPA mode? It is the standard Svelte app scaffold (Vite-based, active ecosystem) and its file-based routing cleanly replaces React Router. Disabling SSR with
adapter-static+prerender = falsegives a pure SPA — identical to the current app.Phase 1 — Scaffold the Svelte App
1a. Replace
apps/web/with SvelteKitapps/web/→apps/web-react/(keep as reference during migration)apps/web/withnpx sv createselecting:adapter-static,prerender = false)apps/web/project.json(Nx targets) to delegate tovite build/vite devtsconfig.app.json:@/*→apps/web/src/*@multiplayer-base/shared-types→libs/shared-types/src/index.tstailwind.config.jsandpostcss.config.jsfrom old app1b. Install new dependencies
Remove:
react,react-dom,react-router-dom, all@radix-ui/*,lucide-reactPhase 2 — Migrate Pure-JS Services (no changes needed)
Copy these files verbatim into
apps/web/src/lib/services/:EventManager.ts— WebSocket singletondocumentModelStore.ts— External store (already uses asubscribeinterface compatible with Svelte stores)api.ts— Axios HTTP clientPhase 3 — Auth Store (replaces AuthContext)
New file:
apps/web/src/lib/stores/auth.tsReplace
AuthContext.tsxwith a plain Sveltewritablestore. Components read with$authStore.userinstead ofuseAuth().PrivateRoutebecomes a SvelteKit layout guard in+layout.svelte.Phase 4 — Routing (SvelteKit file-based)
Phase 5 — Core Components
LayoutDocumentView.svelte— replaceuseSyncExternalStorewith Svelte subscription +onMount/onDestroyLayoutRenderer.svelte— replaceReact.lazy+Suspensewith dynamicimport()+{#await}; pure-JS utilities copy unchangedlayoutRegistry.ts— replaceReact.lazy()with plain() => import()returning a PromiseTargetPortal— custom Svelte portal viadocument.getElementById+tick()or Svelte 5mount()PageHeader.svelte— straightforward rewritePhase 6 — UI Components (Radix → Bits UI)
accordion.tsxbits-uiAccordion.*tabs.tsxbits-uiTabs.*button.tsxbits-uiButtonor customtree-view.tsxAccordionPhase 7 — Layout Components
10 layout components rewritten as
.sveltefiles. Key translations:useRefscroll →bind:this+afterUpdatebind:valuereplaces controlled inputsuseState+ Context →writablestore via Svelte contextuseMemo→$:derived statementsPhase 8 — Pages
7 pages rewritten as SvelteKit
+page.sveltefiles usinggoto()for navigation and$page.url.searchParamsfor query params.Phase 9 — Message Components
6 message components rewritten as Svelte components using the Svelte portal solution.
Phase 10 — Registries
Update lazy import syntax in
layoutRegistry.ts,messageRegistry.ts,documentRegistry.ts.Files NOT changed
apps/api/— untouchedlibs/shared-types/— untouchedapps/api-e2e/— untouchedVerification
npm run restart:web— dev server starts on :4200npx nx test api— backend tests still pass