ARC-ADR-007: Migrate frontend-core from Vite + Svelte to Next.js (App Router)¶
Metadata¶
| Field | Value |
|---|---|
| ID | ARC-ADR-007 |
| Status | Proposed |
| Date | 2026-05-25 |
| Deciders | Architecture Review |
| Supersedes | — |
| Superseded by | — |
| Tags | frontend-core, nextjs, svelte, migration, copilotkit, app-router |
Numbering note. This ADR is authored in the
frontend-corespoke. The hub (nickpclarke/AgentArmy) owns the canonicalARC-ADR-###registry;ARC-ADR-001(HITL decision-point pattern) is published, and002/003/006are referenced as proposed by the CopilotKit epic (#12).007is chosen as the next number after the highest referenced ID; confirm it is free against the hub registry before upstreaming. This andARC-ADR-002/003/006are spoke-authored drafts pending hub ratification.
Context and Problem Statement¶
frontend-core is today a Vite + Svelte 5 (TypeScript) single-page application
(vite.config.ts, svelte.config.js, src/App.svelte, src/components/*.svelte). It
ships as static assets served by nginx, which reverse-proxies /api/* to backend-core
and /middle-core/* to middle-core (nginx.conf.template), so the browser makes
same-origin calls and avoids CORS. The typed backend client is generated from
backend-core's OpenAPI contract (src/lib/api/schema.d.ts) and is framework-agnostic.
The CopilotKit generative-UI epic (#12) and every one of its phase issues (#13–#18)
assume a Next.js App Router application already exists — app/layout.tsx,
app/api/copilotkit/route.ts, and React hooks (useCopilotAction,
useCoAgentStateRender, renderAndWaitForResponse, useCopilotReadable,
useCopilotChatSuggestions). No issue covers actually bootstrapping Next.js: issue #14
says "wrap app/layout.tsx" but there is no app/ directory and no Next.js in the repo.
This scaffold/migration step is an unstated prerequisite for the entire epic and is the
gap this ADR closes.
Two hard technical facts force the question:
- CopilotKit's frontend SDK targets React (and Angular) — there is no Svelte SDK.
The seven generative-UI use cases (Phases 1–4) are implemented entirely through React
hooks and React components (
@copilotkit/react-core,@copilotkit/react-ui). They cannot be implemented in Svelte. - The runtime route requires a server.
app/api/copilotkit/route.tsmust run server-side to hostCopilotRuntime+ExperimentalEmptyAdapter, read the signed-in user's JWT, and forward it to middle-core (see ARC-ADR-002, ARC-ADR-003). A Vite build produces static assets with no server runtime; today nginx only does dumb reverse-proxying and cannot read a session or run adapter code.
So the decision is not merely "add CopilotKit" — it is "what frontend stack and migration
strategy lets frontend-core host both a React generative-UI layer and a first-class
server runtime route, without throwing away the working OpenAPI-driven client and its CI
guardrails."
Decision Drivers¶
| # | Driver |
|---|---|
| D1 | CopilotKit generative UI is React/Angular-only; the seven Phase 1–4 use cases cannot be built in Svelte. |
| D2 | The /api/copilotkit runtime route needs a server runtime to host the Empty adapter and forward the user JWT server-side (ARC-ADR-002, ARC-ADR-003). A static SPA cannot. |
| D3 | Preserve the OpenAPI contract pipeline: vendored contract/backend-core.openapi.json, gen:api codegen, and the contract-consumer.yml drift gate must survive the migration unchanged. |
| D4 | Preserve same-origin API access (no new CORS surface from the browser) — backend-core/middle-core CORS stays a backend concern (epic #12 out-of-scope item). |
| D5 | Minimize risk to working features (upload/ingest, search, health, Rust API monitor, business-objects, cockpit) — avoid a flag-day rewrite where avoidable. |
| D6 | Keep one frontend toolchain, build, and deploy path; avoid permanently running two UI frameworks side by side. |
| D7 | Deployment must remain container-friendly (Azure Container Apps today) and not require a proprietary host. |
| D8 | The signed-in JWT must become readable server-side in the route handler (ARC-ADR-002), which the current localStorage-only token model cannot satisfy. |
Considered Options¶
- Incremental migration to Next.js App Router (strangler) (chosen) — bootstrap a Next.js App Router app in this repo, port screens route-by-route from Svelte to React, reuse the framework-agnostic API client and contract pipeline, and retire Vite once the last screen is migrated.
- Big-bang rewrite to Next.js — delete the Svelte app and rebuild every screen in Next.js in a single cut-over.
- Keep Svelte; embed a React "island" only for the copilot — run CopilotKit React components inside the existing Svelte SPA, mounted into a container element.
- Keep Svelte UI; run the CopilotKit runtime in a standalone Node sidecar — solve the server-route need with a separate Node/Express CopilotKit runtime, keep Svelte for all UI.
- Migrate to SvelteKit instead of Next.js — adopt SvelteKit to gain a server runtime while staying in the Svelte ecosystem.
Decision Outcome¶
Option 1 — Incremental migration to Next.js App Router (strangler) is adopted.
frontend-core becomes a Next.js (App Router, TypeScript) application. The migration is
sequenced so the app is shippable at every step:
- Bootstrap (new Phase 0 prerequisite story — must precede #13/#14). Scaffold Next.js
App Router alongside the existing Svelte app: add
app/layout.tsx,app/page.tsx, a Next config withrewrites()replicating the current Vite/nginx proxies (/api/*→ backend-core,/middle-core/*→ middle-core), and the@copilotkit/*dependencies. Move the JWT fromlocalStorageto a server-readable httpOnly session cookie so the route handler can read it (D8 / ARC-ADR-002). Reusesrc/lib/api/(theopenapi-fetchclient and generatedschema.d.ts) unchanged. - Runtime route + provider (#13, #14). Add
app/api/copilotkit/route.ts(CopilotRuntime+ExperimentalEmptyAdapter+copilotRuntimeNextJSAppRouterEndpoint, JWT forwarded — ARC-ADR-002/003) and wrap the layout in<CopilotKit>with a globalCopilotSidebar. - Port screens route-by-route (Phases 1–4 + existing features). Reimplement
UploadZone,SearchBox,ResultCard,HealthBar,RustApiStatus,BusinessObjectCatalog, andCockpitas React components, one route at a time, adding the matching generative-UI hooks as each phase lands. - Retire Vite/Svelte. Once the last screen is ported, remove
svelte,@sveltejs/vite-plugin-svelte,svelte.config.js,vite.config.ts, and the static nginxtry_filesserving; switch the container image and Azure Container Apps target to run the Next.js Node server.
Out of scope of this ADR: the standalone agentarmy-console/ sub-app (separate
Vite+Svelte project deployed to Azure Static Web Apps) is not migrated here; it has no
CopilotKit dependency and stays as-is. backend-core/middle-core CORS, agent logic, and RBAC
remain backend concerns (epic #12 out-of-scope).
Confirmation Criteria¶
- A Next.js App Router app builds and serves the existing feature set (upload, search,
health, Rust monitor, objects, cockpit) at parity, with
/api/*and/middle-core/*reaching the cores same-origin vianext.configrewrites. npm run gen:apiand.github/workflows/contract-consumer.ymlrun unchanged againstsrc/lib/api/schema.d.ts; the drift gate still fails on contract drift.GET /api/copilotkitreturns 200 and the signed-in JWT is read server-side from the session and forwarded to middle-core (verifiable against a mocked/copilotkitendpoint; full end-to-end requires middle-core running — see cross-repo caveat below).- No
svelte/viteruntime dependency remains inpackage.jsonafter step 4; the production container runs the Next.js server, not static nginx. - Bundle analysis shows no LLM API key in any client bundle (ARC-ADR-003).
Pros and Cons¶
Option 1 — Incremental migration to Next.js App Router (chosen)¶
Pros:
- Satisfies D1 and D2 directly: Next.js App Router is CopilotKit's first-party host for both
React generative UI and the server runtime route (
copilotRuntimeNextJSAppRouterEndpoint). - Strangler sequencing keeps the app shippable throughout; reduces big-bang risk (D5).
next.configrewrites()reproduce the same-origin proxy model, preserving D4 with no new browser CORS surface.- The OpenAPI client and contract CI are framework-agnostic and carry over untouched (D3).
- Ends on a single toolchain once Vite is retired (D6); container-deployable to Azure Container Apps (D7).
Cons:
- A transition window runs two build systems (Vite + Next.js) until the last screen ports — temporary tooling/CI complexity.
- React reimplementation of every Svelte component is real effort, not a mechanical port.
- Requires the JWT/session change (D8), touching the auth flow before any copilot value lands.
Option 2 — Big-bang rewrite to Next.js¶
Pros:
- No dual-toolchain transition window; one clean codebase at the end.
- Forces a coherent App Router structure from day one.
Cons:
- Violates D5: a flag-day cut-over risks regressing every working feature at once with no incremental safety net.
- Long lead time before anything (including Phase 0) ships; blocks the epic on a full rewrite rather than a thin scaffold.
Option 3 — Keep Svelte; embed a React island for the copilot only¶
Pros:
- Smallest change to existing UI; Svelte screens keep working untouched.
Cons:
- Permanently runs two UI frameworks (violates D6): doubled tooling, two component models, hydration/bundle overhead.
- Generative-UI use cases need to drive the host app —
useCopilotReadableshares filter state,useCopilotActionnavigates/filters the UI (#18). A React island cannot cleanly read or mutate Svelte component state, so Phases 3–4 become brittle bridge code. - Still does not provide a server runtime for the route (D2 unmet) — needs Option 4 bolted on.
Option 4 — Keep Svelte UI; CopilotKit runtime in a standalone Node sidecar¶
Pros:
- Solves the Phase 0 server-route need (D2) without touching the Svelte UI.
- CopilotKit's runtime does support non-Next hosts (Node HTTP / Express / NestJS), so this is technically viable for the route alone.
Cons:
- Does nothing for D1: the generative UI (Phases 1–4) still can't be built in Svelte.
- Adds a second deployable service and its own auth/session plumbing for JWT forwarding — more moving parts, not fewer.
- A dead end: it unblocks only the spike, then the same React/Next.js decision returns for Phases 1–4.
Option 5 — Migrate to SvelteKit instead of Next.js¶
Pros:
- Gains a server runtime (D2) while staying in the Svelte ecosystem; smaller mental shift for the current code.
Cons:
- Violates D1 decisively: CopilotKit has no Svelte frontend SDK, so the generative-UI hooks and components — the entire point of the epic — simply do not exist for SvelteKit.
- Every CopilotKit example, the hub plan, and all phase issues are written for Next.js App Router; choosing SvelteKit means maintaining a bespoke integration with no upstream support.
Positive Consequences¶
- The epic's unstated prerequisite is made explicit and ownable: a single bootstrap story
precedes #13/#14, so teams stop assuming
app/exists. - The repo lands on CopilotKit's supported, documented happy path (Next.js App Router), minimizing integration risk for Phases 0–4.
- Same-origin access and the contract-driven client — the two load-bearing properties of the current app — are preserved across the migration.
Negative Consequences¶
- Net new framework expertise (React + Next.js App Router, RSC/SSR caveats) is required of a team currently writing Svelte.
- The auth model changes (localStorage token → httpOnly session cookie); this must land early and is a prerequisite for ARC-ADR-002, adding coupling between this migration and the JWT contract.
- Deployment shifts from static-nginx to a Node server image; the
Dockerfile,docker-compose.yml, and Azure Container Apps revision need updating, and runtime cost profile changes from static hosting to an always-on Node process.
Implementation Notes¶
New Phase 0 prerequisite story (does not exist yet — create before #13/#14):
"Bootstrap Next.js App Router shell in frontend-core" covering: Next.js + @copilotkit/*
install, app/layout.tsx/app/page.tsx, next.config rewrites mirroring
vite.config.ts/nginx.conf.template, httpOnly session cookie for the JWT, and CI wiring
so gen:api + contract-consumer.yml keep passing.
Carry-over (do not rewrite): src/lib/api/client.ts + schema.d.ts (openapi-fetch is
framework-agnostic); contract/backend-core.openapi.json; the gen:api* scripts;
contract-consumer.yml.
Auth change (links ARC-ADR-002): src/lib/api/client.ts reads the token from
localStorage (backend-core-token) and attaches it in the browser. For the route handler
to forward the JWT server-side, the token must live in a server-readable httpOnly cookie /
session. Plan this as part of bootstrap, not as an afterthought.
Proxy parity: translate the two Vite proxies (/api → backend, /middle-core →
middle-core with prefix rewrite) and the matching nginx location blocks into
next.config.js rewrites() so the browser stays same-origin (D4).
Deployment: replace the static-nginx image with a Next.js Node server image; keep Azure
Container Apps as the target (D7). The reverse-proxy responsibilities move from nginx into
next.config rewrites; nginx is no longer required for routing.
Related Decisions¶
- Enables / is required by: ARC-ADR-002 (JWT-forwarding) and ARC-ADR-003 (no LLM key / Empty adapter) — both presuppose the Next.js server runtime this ADR introduces.
- Enables: ARC-ADR-006 (HITL destructive ops) — the
renderAndWaitForResponsepattern is a React/CopilotKit capability available only after this migration. - Relates to: Epic #12 and phase issues #13–#18 (this ADR supplies their missing
prerequisite); hub plan
docs/plans/copilotkit-generative-ui.md(assumes Next.js). - Does not affect:
agentarmy-console/(separate Vite+Svelte app, Azure Static Web Apps) — intentionally excluded.
Open Questions / Caveats¶
- Cross-repo dependencies are unverifiable from this spoke. middle-core
/copilotkitbeing live and backend-core CORS are in private repos; the FE migration can be built and validated against a mocked/copilotkit, but the true end-to-end smoke test (epic acceptance) needs middle-core running. - Confirm
ARC-ADR-007is free in the hub registry before upstreaming (see numbering note).
Revision History¶
| Version | Date | Author | Change |
|---|---|---|---|
| 0.1 | 2026-05-25 | Architecture Review | Initial proposal (spoke draft) |