Skip to content

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-core spoke. The hub (nickpclarke/AgentArmy) owns the canonical ARC-ADR-### registry; ARC-ADR-001 (HITL decision-point pattern) is published, and 002/003/006 are referenced as proposed by the CopilotKit epic (#12). 007 is chosen as the next number after the highest referenced ID; confirm it is free against the hub registry before upstreaming. This and ARC-ADR-002/003/006 are 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 existsapp/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:

  1. 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.
  2. The runtime route requires a server. app/api/copilotkit/route.ts must run server-side to host CopilotRuntime + 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

  1. 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.
  2. Big-bang rewrite to Next.js — delete the Svelte app and rebuild every screen in Next.js in a single cut-over.
  3. Keep Svelte; embed a React "island" only for the copilot — run CopilotKit React components inside the existing Svelte SPA, mounted into a container element.
  4. 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.
  5. 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:

  1. 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 with rewrites() replicating the current Vite/nginx proxies (/api/* → backend-core, /middle-core/* → middle-core), and the @copilotkit/* dependencies. Move the JWT from localStorage to a server-readable httpOnly session cookie so the route handler can read it (D8 / ARC-ADR-002). Reuse src/lib/api/ (the openapi-fetch client and generated schema.d.ts) unchanged.
  2. 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 global CopilotSidebar.
  3. Port screens route-by-route (Phases 1–4 + existing features). Reimplement UploadZone, SearchBox, ResultCard, HealthBar, RustApiStatus, BusinessObjectCatalog, and Cockpit as React components, one route at a time, adding the matching generative-UI hooks as each phase lands.
  4. Retire Vite/Svelte. Once the last screen is ported, remove svelte, @sveltejs/vite-plugin-svelte, svelte.config.js, vite.config.ts, and the static nginx try_files serving; 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 via next.config rewrites.
  • npm run gen:api and .github/workflows/contract-consumer.yml run unchanged against src/lib/api/schema.d.ts; the drift gate still fails on contract drift.
  • GET /api/copilotkit returns 200 and the signed-in JWT is read server-side from the session and forwarded to middle-core (verifiable against a mocked /copilotkit endpoint; full end-to-end requires middle-core running — see cross-repo caveat below).
  • No svelte/vite runtime dependency remains in package.json after 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.config rewrites() 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 appuseCopilotReadable shares filter state, useCopilotAction navigates/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.


  • 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 renderAndWaitForResponse pattern 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 /copilotkit being 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-007 is 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)