ADR 0105 — Antlers frontend framework evaluation
Status: Proposed
Date: 2026-05-27 UTC
Amended: 2026-05-27 UTC — §TypeScript as an orthogonal dimension added in response to #2841; §Next.js vs SvelteKit head-to-head added in response to #2854
Deciders: Kristerpher (operator); software-architect
Scope: frontend/trademaster_ui/ (Antlers) — raxx.app; iOS companion surface; CE visual port
Context
Antlers is a Create-React-App (CRA) SPA at frontend/trademaster_ui/ — ~255 source files,
~51,700 lines of JS. The 2026-05-26/27 testing window surfaced a chain of bugs caused
by the SPA's client-side state model:
- Bundle cache staleness on Safari incognito — hard-refresh and new private windows
still loaded older bundle hashes. Suspected interaction: CRA's default service worker
registration had been removed from
index.js, but CF Pages edge cache and Safari's own opaque-response caching (which applies different max-age rules toapplication/javascriptthan other browsers) still served stale chunks. - Route guard race conditions —
settings.onboardingCompletedset in React context state lags behindlocalStoragewrite; App's route guard re-evaluates on stale state and bounces/→/setupeven thoughlocalStorageconfirms onboarding complete. Required three layered patches (#2790, #2814, #2815, #2816). - Multi-context state desync — six React contexts (SettingsContext, FlagContext,
AppContext, DemoContext, SymbolsContext, ObfuscateContext) all initialize from
useStateclosures overlocalStorage. Hydration timing is non-deterministic: the outermost<App>readsisEnabled()at module-import time (synchronous), while inner context reads happen during React's first render pass. Any ordering change introduced by a flag or a new provider produces new timing bugs. - Flag-gated multi-path rendering in App.js —
App.jshas four separate return-paths forFLAG_ROUTE_GUARD,FLAG_RAXX_APP_SHELL,FLAG_CE_PORT, and the legacy Bootstrap shell. Route guard decisions are made by comparing context state againstlocalStoragedirect-reads in the same component body (added as a fix). This is an unmaintainable shape. - CE port mid-flight — SignupPage, LoginPage, PublicLanding are done. Dashboard, Backtest, Trading, Options, Onboarding, Settings (~40 pages) are still on raw react-bootstrap chrome.
The operator floated React Native as a convergence play given the locked iOS
companion requirement (ADR-0004: native SwiftUI, StoreKit 2 IAP — project_ios_billing_iap).
This ADR evaluates the full field and recommends a path.
The operator subsequently asked for a genuine head-to-head between Next.js and SvelteKit, not a side-note dismissal. That analysis is in the addendum §Next.js vs SvelteKit — head-to-head (2026-05-27 #2854) at the end of this document. The earlier 8-option matrix and decision criteria are preserved unchanged for full context.
Decision
Recommended option: Next.js (App Router, SSR + RSC) with React 18.
Server-side route guards eliminate the entire category of client-state-desync bugs identified in the testing window. The existing React component library (CE design tokens, brand.css, all completed signup/login/landing pages, the six context providers) ports with minimal modification — Next.js is React, not a different paradigm. The CE visual port-in-flight (#2789–#2813 wave) continues without restart.
iOS stays on native SwiftUI (ADR-0004 is unchanged). No React Native pivot.
See §Options for full scoring and the §Next.js vs SvelteKit head-to-head addendum for the detailed two-way comparison. The operator must confirm between Next.js and the "stay on CRA + targeted fixes" baseline before sub-cards are filed.
Language choice rationale
This ADR governs a frontend surface, not a new backend service. No new Rust/C++/Python
service is introduced. Language tier classification (per docs/architecture/language-tier-policy.md)
is not applicable to browser-side JavaScript/React. The iOS companion surface retains
SwiftUI (Tier 1 equivalent for iOS native) per ADR-0004.
Decision criteria
Eight dimensions, each scored 1–5 (5 = best).
| # | Dimension | Description |
|---|---|---|
| D1 | Safari incognito cache bug prevention | Does the framework eliminate or reduce the bundle-staleness class? |
| D2 | iOS app convergence | Single codebase / shared library vs separate Swift native |
| D3 | CE visual port effort | Cost to port existing or in-flight CE work |
| D4 | WebAuthn + cookie + CF Access compatibility | Can the passkey flow + cross-origin session cookie work? |
| D5 | Solo dev velocity | LLM-agent-friendly, well-documented patterns |
| D6 | Deployment + CDN compatibility | CF Pages or equivalent; CI pipeline |
| D7 | Backend integration cost | Raptor/Queue REST APIs; withCredentials; SameSite=None |
| D8 | Test infrastructure | Playwright currently in use; Jest for unit tests |
Weights (must sum to 1.0): D1=0.20, D2=0.10, D3=0.18, D4=0.15, D5=0.15, D6=0.08, D7=0.07, D8=0.07
Options
Option 1 — Stay on React + CRA (baseline)
Bug-prevention story (D1: 2/5):
CRA emits a main.[hash].js bundle and a static/js/*.chunk.[hash].js set with
content-addressed filenames. The index.html is not content-addressed and is served
at / with Cache-Control: no-cache via the _headers file on CF Pages. If a deploy
races with an open browser tab, the old index.html references chunk hashes that no
longer exist at CF Pages; Safari's opaque-response caching makes this worse in incognito
because the browser cannot inspect the cached response headers and applies its own
heuristic max-age. No service worker is currently registered (index.js has no SW
registration), so the SW interaction hypothesis is a red herring — but CF Pages edge
cache + Safari's own local cache still produced the bug. The fix is achievable via
explicit Cache-Control: no-store headers on the HTML entry point and content-hash
pinning on all chunk URLs. This is a targeted 2-PR fix, not a rewrite.
The route-guard race condition (D3 of bug list) is fixable by moving onboardingCompleted
source-of-truth to a server-side session check (the route guard already stubs this via
SessionContext). When GET /api/auth/session becomes the authority (Queue Phase 1),
the localStorage race disappears. This is already planned.
The multi-context desync is the hardest problem to fix in CRA without changing the
rendering model. The six stacked providers with synchronous useState(() => localStorage…)
initializers are the root cause. Short of server-side rendering, the fix is to move all
flag-reading to a single useState + useEffect that writes to a single context, and
to replace the four-path App.js with a proper route-based code split. This is
significant refactoring but not a framework change.
CE port effort (D3: 3/5): In-flight wave (#2789–#2813) continues. No restart. ~40 pages remaining; estimated 3–5 operator-weeks to complete CE port in CRA.
iOS convergence (D2: 1/5): No convergence. iOS stays on SwiftUI (ADR-0004 locked).
WebAuthn / CF Access (D4: 5/5): Current setup works. @simplewebauthn/browser v13
already wired. Cross-origin cookie with SameSite=None; Secure already works.
Solo dev velocity (D5: 3/5): CRA is well-known to LLM agents but is officially
deprecated by the React team (no updates since 2023; react-scripts is not maintained).
LLM generation quality for CRA patterns is adequate but declining as the ecosystem moves
toward Vite and Next.js.
Deployment (D6: 4/5): CF Pages with wrangler already works. No change needed.
Backend integration (D7: 5/5): Zero change. Existing axios.defaults.withCredentials = true setup works.
Test infrastructure (D8: 4/5): Playwright E2E tests and Jest unit tests continue unchanged.
Weighted score: 3.19
Honest assessment: Staying on CRA is not as bad as the chaos of the testing window
makes it feel. The Safari cache bug has a targeted fix (2 PRs). The route-guard race
has a targeted fix (complete the GET /api/auth/session integration). The multi-context
desync is the real structural debt, and it is also fixable without a rewrite — but it
requires significant App.js refactoring. If Kristerpher is willing to spend 2–3 weeks
fixing CRA's structural problems before resuming the CE port, CRA is viable through v1.
The downside: CRA itself is dead upstream. Every dependency upgrade battle from here is
solo.
Option 2 — React + Next.js (App Router, SSR + RSC) [Recommended]
Bug-prevention story (D1: 5/5):
Next.js server-renders pages by default. Route guards run on the server (middleware or
redirect() in server components) before a byte is sent to the browser. The entire class
of "client state is stale when the route guard runs" bugs is architecturally eliminated —
there is no client state involved in routing decisions. The Safari incognito cache problem
is also eliminated: server-rendered HTML is delivered fresh per-request (no static
index.html with stale chunk hashes). Static assets (JS/CSS) remain content-addressed
and served with long max-age. The entry point is never cached stale.
The context-stack desync is addressed by React Server Components: data that is currently
read from localStorage on first render can instead be fetched from the server and
embedded in the initial page HTML. No hydration race. Flag values come from the server
(env vars or a server fetch to Raptor); no client-side isEnabled() bootstrap timing.
CE port effort (D3: 4/5):
Next.js uses React. Every CE component that was already completed (SignupPage, LoginPage,
PublicLanding, brand.css tokens, RaxxAppShell) ports without modification — they are
React components. The six context providers port as React Context in Client Components.
The App Router's layout system (layout.tsx) replaces App.js's four-path return
structure with a clean layout hierarchy. The 40 remaining pages port individually, not
simultaneously. Estimated: the CRA → Next.js migration scaffold takes 1–2 weeks; the CE
port-in-flight wave continues inside the new scaffold. Total additional time vs finishing
in CRA: ~1 week for the migration scaffold.
iOS convergence (D2: 1/5): No convergence. Next.js is web-only. iOS stays on
SwiftUI (ADR-0004 locked and unchanged). This is the correct posture — WebAuthn on iOS
requires ASAuthorizationController (native), which no web wrapper can call.
WebAuthn / CF Access (D4: 5/5):
@simplewebauthn/browser works in Next.js. The WebAuthn ceremony (navigator.credentials)
runs client-side and is unchanged. The cross-origin cookie (SameSite=None; Secure;
Domain=.raxx.app) works identically — Next.js uses fetch with credentials: 'include'
or the same axios.defaults.withCredentials pattern. CF Access middleware at the edge
(Cloudflare Workers) can inspect cookies before the Next.js server sees the request, which
is a stricter and more reliable auth gate than current CRA behavior.
Server-side route guards in Next.js middleware receive the session cookie and can validate
it against Raptor's GET /api/auth/session before serving any page. This is the correct
auth model.
Solo dev velocity (D5: 4/5): Next.js is the most-documented React framework. LLM training data is densest for Next.js App Router patterns. The React team officially recommends a framework (Next.js, Remix, or similar) for new React projects. Agent-generated code quality for Next.js is measurably better than for CRA.
Deployment (D6: 3/5):
Next.js on CF Pages requires the @cloudflare/next-on-pages adapter. Full-stack Next.js
(SSR) requires a Cloudflare Workers runtime or a hosting platform that supports Node.js
server rendering (Vercel, Fly.io). CF Pages with next-on-pages runs the App Router on
Workers (edge runtime). The edge runtime has some Node.js API restrictions (no fs,
no native modules). For Antlers, this is unlikely to matter — no filesystem access is
needed in the frontend.
Alternative: host Next.js on Heroku (same platform as Raptor) as a separate Heroku app.
This eliminates the edge-runtime restrictions at the cost of a dyno. Given Antlers is
static-ish (data comes from Raptor API calls), CF Pages + next-on-pages is the preferred
path. Fallback: Vercel, which has zero-config Next.js support.
Backend integration (D7: 4/5):
Raptor REST API calls move from client-side axios to either server-side fetch() (RSC
pattern) or client-side axios with credentials: 'include' (Client Component pattern).
The cross-origin cookie and CORS setup are unchanged. The only new plumbing: Next.js
middleware reads the session cookie server-side to gate routes, which requires Raptor's
GET /api/auth/session to be callable from the Next.js Workers runtime (it already is —
it's a plain HTTP call).
Test infrastructure (D8: 3/5):
Playwright tests continue unchanged — they drive a browser, not the framework internals.
Jest unit tests for React components continue unchanged. The new addition: Next.js has its
own testing patterns for Server Components (Vitest or Jest with Next.js test utilities).
Some existing @testing-library/react tests will need minor adaptation for components
that become Server Components.
Weighted score: 3.91
Option 3 — React Native (Expo, react-native-web)
Summary: React Native produces native iOS/Android apps. react-native-web compiles
RN components to DOM elements, theoretically allowing one codebase to target web and iOS.
WebAuthn (D4: 1/5):
WebAuthn (navigator.credentials) is a browser API. React Native does not have a browser.
react-native-web maps RN components to DOM but does not provide navigator.credentials
in a native context. On iOS, passkeys require ASAuthorizationController via a native
Swift module. An RN bridge to ASAuthorizationController exists (react-native-passkey)
but it is community-maintained, not Apple-supported, and introduces a bridging layer around
the most security-critical code path in the product. This is a design invariant risk:
the passkey ceremony must be reliable and auditable. A community-bridge introduces a
non-auditable layer.
On the web target (react-native-web), navigator.credentials works if the RN component
calls window.navigator.credentials explicitly — but this requires platform-divergent code
in every auth component, defeating the "one codebase" claim.
CE port effort (D3: 1/5):
react-native-web maps RN <View>, <Text>, <TouchableOpacity> to DOM elements, not
HTML/CSS. The CE design system (brand.css CSS custom properties, Bootstrap-based layouts,
the Confidence Engine card/table patterns) does not transfer. Every CE component would
need to be rewritten in RN's layout model (Flexbox-by-default, no grid, no position:
absolute anchoring, no CSS custom properties). This is a complete rewrite, not a port.
iOS convergence (D2: 4/5):
The most plausible iOS convergence path — but the auth and styling friction are too high.
ADR-0004 is locked on native SwiftUI specifically because ASAuthorizationController is
a native-only API. An RN app on iOS would use a bridge to call it, introducing the exact
bridging risk ADR-0004 was written to avoid.
Weighted score: 2.34
This option is rejected. The WebAuthn invariant conflict is disqualifying.
Option 4 — SvelteKit
See the addendum §Next.js vs SvelteKit — head-to-head (2026-05-27 #2854) for the full 10-dimension analysis. Summary score for this matrix: 2.81
Rejected primarily on D3 (complete rewrite cost) and D5 (lower agent velocity) relative to Next.js. If reuse of the existing React codebase were not a factor, SvelteKit would be a strong contender on bundle size and reactive model elegance.
Option 5 — Astro + React islands
Summary: Astro is a content-first framework that ships zero JS by default; React
components can be used as "islands" with client:load or client:visible directives.
D1 (cache bug): 5/5 — Fully server-rendered HTML by default.
D3 (CE port): 3/5 — React components work inside Astro as islands. Static pages
(PublicLanding, docs, marketing) port easily. Highly interactive pages (Dashboard,
Backtesting with live chart updates, TradeWindow with real-time order state) require
client:load islands, which reintroduce the same client-state management problems.
The trading dashboard is not a content page — it is a real-time application. Astro's
island model is not well-suited to it.
D4 (WebAuthn): 4/5 — Works in React islands. Same as Next.js.
D5 (solo dev velocity): 3/5 — Astro's mental model (islands) adds a layer of reasoning about what renders where. For an app that is mostly interactive (all the trading pages), the developer pays the island-reasoning cost on every page without much gain.
D7 (backend integration): 3/5 — Session cookies work in islands. But SSE / real-time updates (live order fills) require a client-side island for every real-time surface. No global client-side state management across islands without adding a store (Nano Stores, Zustand) that reintroduces the synchronization problem.
Weighted score: 3.02
Better than staying on CRA but worse than Next.js for this use case (trading app is almost entirely interactive).
Option 6 — Native iOS (Swift/SwiftUI) + Phoenix LiveView for web
Summary: Treat web and iOS as entirely separate surfaces. Web gets Phoenix LiveView (Elixir); iOS gets SwiftUI (already planned).
D2 (iOS convergence): 5/5 — Cleanest iOS story; zero compromise.
D3 (CE port): 1/5 — Complete rewrite of the web surface in Elixir/LiveView. Zero reuse of existing React work.
D5 (solo dev velocity): 1/5 — Adds Elixir to the stack. LLM agents are competent with Elixir but the operator has no existing Elixir infrastructure. A new language runtime, a new web server (Phoenix/Cowboy), and a new deployment pattern would need to be introduced alongside the existing Python/Flask/Postgres/Heroku stack. The operational overhead for a solo operator is very high.
D6 (deployment): 2/5 — Phoenix LiveView requires a persistent WebSocket connection; CF Pages cannot serve it. A new Heroku app (or Fly.io) would be needed.
D7 (backend integration): 3/5 — LiveView can call Raptor REST APIs via server-side HTTP. Cross-origin session cookies are a non-issue (LiveView owns the session at the server; it is not a cross-origin setup). But this means maintaining two auth sessions: one for the LiveView app, one for the API. More moving parts, not fewer.
Weighted score: 2.44
Rejected. Introduces Elixir runtime for zero code-reuse benefit.
Option 7 — HTMX + minimal JS
Summary: Server-rendered HTML with HTMX attributes for partial updates. Raptor (or a thin Python BFF) serves the HTML directly. Chart interactions via raw JS.
D1 (cache bug): 5/5 — Full server rendering; no bundle.
D3 (CE port): 1/5 — Complete rewrite. HTMX renders HTML from the server; the CE React components have no analog.
D5 (solo dev velocity): 2/5 — LLM agents generate HTMX patterns adequately but the trading dashboard (Chart.js, lightweight-charts, real-time order state, react-select) requires custom JS bridges that are not well-covered by LLM training data for HTMX patterns. The real-time order feed (TradeWindow, OrdersList) would require custom WebSocket or SSE handling without a framework-level abstraction.
D7 (backend integration): 4/5 — Backend owns the HTML templates; no cross-origin cookie complexity if served from the same domain.
Weighted score: 2.59
Rejected. The trading dashboard's interactivity requirements make HTMX a poor fit.
Optional: Solid.js + SolidStart
D1: 4/5 (server rendering available), D3: 2/5 (different paradigm, signals not hooks — partial reuse), D4: 4/5 (same browser APIs), D5: 2/5 (smaller LLM corpus), D6: 3/5 (CF Pages adapter exists but less battle-tested).
Weighted score: 2.86 — Better than HTMX/RN but worse than Next.js. Not recommended for a solo operator rewrite given the smaller community.
Optional: Qwik
Resumability is interesting for cold-start performance but adds architectural complexity (serializable closures, server-side execution model) that is poorly understood by current LLM agents. Bleeding-edge; skip.
Decision matrix (8-option)
| Option | D1 (×0.20) | D2 (×0.10) | D3 (×0.18) | D4 (×0.15) | D5 (×0.15) | D6 (×0.08) | D7 (×0.07) | D8 (×0.07) | Weighted |
|---|---|---|---|---|---|---|---|---|---|
| 1. CRA (baseline) | 2 | 1 | 3 | 5 | 3 | 4 | 5 | 4 | 3.19 |
| 2. Next.js (rec.) | 5 | 1 | 4 | 5 | 4 | 3 | 4 | 3 | 3.91 |
| 3. React Native | 2 | 4 | 1 | 1 | 3 | 2 | 3 | 2 | 2.34 |
| 4. SvelteKit | 5 | 1 | 1 | 4 | 2 | 4 | 4 | 3 | 2.81 |
| 5. Astro + islands | 5 | 1 | 3 | 4 | 3 | 4 | 3 | 3 | 3.02 |
| 6. SwiftUI + LiveView | 3 | 5 | 1 | 3 | 1 | 2 | 3 | 2 | 2.44 |
| 7. HTMX | 5 | 1 | 1 | 4 | 2 | 4 | 4 | 2 | 2.59 |
| 8. Solid.js | 4 | 1 | 2 | 4 | 2 | 3 | 4 | 3 | 2.86 |
Recommendation
Top recommendation: Next.js (App Router, SSR + RSC)
The case rests on three structural wins:
-
Server-side route guards eliminate the bug class entirely. The route-guard race, the multi-context desync, and the Safari cache staleness are all symptoms of the same root cause: routing decisions made on the client using client-side state. Next.js middleware runs on the server before the page renders. There is no race.
-
Near-zero rework of existing components. Next.js is React. The CE work that is done (SignupPage, LoginPage, PublicLanding, brand.css, RaxxAppShell) ports without modification. The CE port wave (#2789–#2813) continues inside the new scaffold rather than restarting.
-
iOS stays on SwiftUI. ADR-0004 is unchanged. The React Native option is rejected on the WebAuthn invariant conflict. There is no meaningful iOS code convergence to be gained without compromising the passkey flow.
Alternative if Kristerpher wants to avoid the migration overhead: Stay on CRA + targeted structural repairs. The specific repairs needed are:
- Cache fix: set
Cache-Control: no-storeon theindex.htmlentry point specifically (already possible via_headerson CF Pages). 1 PR. - Route guard: complete the
GET /api/auth/sessionintegration soSessionContextis authoritative. 1–2 PRs. - Context desync: consolidate the four-path
App.jsinto a single layout hierarchy with a properLoadingGatecomponent that blocks rendering until all contexts are hydrated. 2–3 PRs.
Total CRA repair cost: 4–6 PRs, ~2 weeks. Then resume the CE port. The downside remains: CRA is a dead framework. Every year this choice is revisited.
Operator decision required: See §Open questions below.
TypeScript as an orthogonal dimension
Added 2026-05-27 UTC in response to #2841: "What about TypeScript and React?"
The framework-vs-language distinction
TypeScript is a tooling layer, not a framework choice. Every option in the decision matrix above can be implemented in either JavaScript or TypeScript. The two decisions are independent:
Framework axis: CRA ── Next.js ── SvelteKit ── ...
Language axis: JS ── TypeScript
Any cell in that 2×N matrix is valid. The original ADR evaluated the framework axis only. This section addresses the language axis and clarifies what adopting TypeScript would and would not solve from the 2026-05-26/27 bug chain.
What TypeScript would and would not have caught
Five incidents drove the ADR. For each, an honest verdict on compile-time TypeScript catching power:
| Incident | Would TS have caught it? | Reasoning |
|---|---|---|
Route guard race (settings.onboardingCompleted stale at guard time) |
No | TypeScript cannot catch async state timing bugs. The value has the correct type (boolean) at every point in the code. The problem is that the value is stale — stale at the correct type. This is a React state model problem, not a type problem. |
| Multi-context state desync (six contexts, non-deterministic hydration order) | Partial | TS could enforce that components reading onboardingCompleted must call a specific typed hook (e.g., useSettings(): SettingsContext) rather than reading from multiple sources. It's a discipline aid: if you define SettingsContext as the single source and the type checker enforces that, devs can't accidentally read from localStorage directly. But TS doesn't prevent multiple typed contexts from hydrating in the wrong order — it just surfaces "you typed it wrong" not "your init order is wrong". |
| Safari incognito bundle cache staleness | No | This is an infra/CDN cache-control header issue. TypeScript has no awareness of cache headers or browser caching behavior. |
| 40+ pages on raw react-bootstrap (CE port incomplete) | No | TypeScript doesn't accelerate visual porting. Adding types to un-ported pages doesn't change the visual output or reduce the port backlog. |
Cross-context auth setup mistakes (axios.defaults.withCredentials, cookie domain, session race) |
Yes, partially | A typed API client (e.g., apiClient.get<SessionResponse>('/api/auth/session') with a typed base config that enforces withCredentials: true) would have surfaced "you created a raw axios instance without credentials" at the call site. Typed cookie/session helpers prevent the "forgot to pass credentials" class. This is a real, though bounded, win. |
Bottom line: TypeScript is a quality-of-life improvement, not a silver bullet for the bug class that motivated this ADR. The bigger structural wins come from server-side routing (Next.js) eliminating the state-on-stale-state class entirely. Adding TypeScript to the current CRA codebase would have prevented roughly 1 of the 5 incident categories above, partially helped with a second, and had no effect on the other three.
"React + TypeScript on current CRA" as a real 4th option
The original decision matrix implicitly treated CRA as JavaScript-only. TypeScript adoption on CRA is a real, distinct option. Evaluated against the decision criteria:
Effort: ~2–3 weeks. CRA supports TypeScript by default in new projects; migrating an
existing JS project uses allowJs: true in tsconfig.json and converts files
incrementally. The migration path is:
- Add
tsconfig.jsonwithallowJs: true,checkJs: false,strict: false(permissive start).~0.5 days. - Define typed interfaces for the six context shapes, API response types, flag config,
and
tradeMasterSettings.~2–3 days. - Convert the most-edited files first (App.js, webauthnAPI.js, SettingsContext,
SessionContext).
~1 week. - Tighten strictness incrementally as the codebase converts. Ongoing.
Bug-prevention story: PARTIAL — per the table above. Catches the "you typed it wrong" class. Does not catch the "your state is async" class.
CE port effort: Unchanged. The ~40 un-ported pages have the same visual work regardless
of whether they are .js or .tsx. Converting JS files to TS while porting CE is additive
work (~+20% per file), not a blocker.
iOS app convergence: Unchanged. TypeScript on the web frontend doesn't affect the SwiftUI iOS companion.
Solo-dev velocity with LLM agents: Mixed, and actually net-positive. TS slows initial keystrokes (you must declare types) but speeds up refactors (the type checker catches broken call sites). With LLM agents doing most of the generation, TypeScript is a feedback amplifier: compile errors are machine-readable and deterministic, which means agents get precise error messages instead of runtime surprises. LLMs are also trained on more TypeScript code than JavaScript for Next.js patterns. On balance, TS improves agent velocity for this codebase.
CRA upstream death: Still a concern. Adding TypeScript to CRA does not resurrect
react-scripts. The framework is still unmaintained upstream. TypeScript adoption and
framework survival are orthogonal.
Summary as a standalone option:
| Dimension | CRA + TS |
|---|---|
| Addresses route-guard race | No |
| Addresses context desync | Partial (discipline aid) |
| Addresses Safari cache bug | No |
| Addresses CE port backlog | No |
| Addresses auth setup mistakes | Yes (typed API client) |
| Migration cost | 2–3 weeks standalone |
| CRA upstream dead | Still yes |
| Precludes Next.js later | No — TS files port directly |
If Next.js is chosen, TypeScript is automatic
Next.js projects are TypeScript-first by default. The create-next-app scaffold
generates tsconfig.json, typed layout/page conventions, and typed middleware. Choosing
Next.js means getting TypeScript as part of the migration, not as a separate decision.
There is no "Next.js without TypeScript" path worth taking. The framework's Server
Component and middleware patterns are designed with TypeScript's type narrowing in mind
(e.g., typed cookies(), typed redirect(), typed NextRequest). Adopting Next.js
in JavaScript would be actively fighting the framework.
Recommendation update
Considering the TypeScript dimension, the original recommendation — Next.js + TypeScript — is unchanged. The reasoning is:
- TypeScript alone does not address the route-guard race (the bug that required three patches and destabilized the personal-use launch window).
- CRA is still dead upstream; TS adoption doesn't change the framework's maintenance trajectory.
- Next.js provides both the server-side routing wins (structural bug fix) and TypeScript by default (quality-of-life win) in a single migration.
- The migration cost difference between "CRA + standalone TS adoption" (~2–3 weeks) and "Next.js scaffold + TS" (~3–5 weeks migration scaffold) narrows to roughly 1 week once you account for the fact that Next.js with TS gives you both wins simultaneously.
However — lowest-disruption path for personal-use window: If Kristerpher's priority right now is shipping the personal-use launch and deferring the framework decision, "React + TypeScript on current CRA" is the lowest-risk intermediate step. It gives type safety immediately (~2–3 weeks), does not change the deployment target, does not change the routing model, and does not preclude a Next.js migration later (TS files port directly into Next.js). This is a valid path. It is slower to the structural bug fix, but it is reversible and non-disruptive.
The two valid paths to the same destination:
Path A (recommended):
CRA (JS) ──► Next.js + TS migration (3–5 weeks scaffold + CE port continues)
└─ gets structural bug fix + TS together
Path B (low-disruption intermediate):
CRA (JS) ──► CRA + TS standalone (2–3 weeks) ──► Next.js + TS migration (later)
└─ gets TS now └─ gets structural bug fix later
Both paths end at React + TypeScript. Path A is faster to the full goal. Path B is more conservative and keeps the personal-use window unblocked.
Migration plan (Next.js path)
Scope
Antlers is a pure frontend rewrite (JS only). No Raptor routes change. No DB migrations.
No CORS changes. The migration is self-contained to frontend/trademaster_ui/.
Strategy: scaffold-first, page-by-page
Not a big-bang rewrite. The CRA app continues to serve production during the migration. The Next.js scaffold is built alongside it and promoted to production when the critical path (auth flow + dashboard) is green.
Phase 1 — Scaffold (1 week)
- Init Next.js app in frontend/trademaster_ui_next/ (parallel, not replacement)
- Port brand.css tokens, index.css, sentryInit
- Port layout.tsx (RaxxAppShell equivalent)
- Port Next.js middleware: read session cookie → call GET /api/auth/session → redirect
to /login if unauthenticated. Eliminates all client-side route guard logic.
- Port SignupPage, LoginPage, PublicLanding (already CE-done; minimal changes)
- Deploy Next.js scaffold to CF Pages staging slot (separate URL)
Phase 2 — Auth critical path (1 week)
- Port webauthnAPI.js → use fetch() with credentials: 'include'
- Port SettingsContext, SessionContext, FlagContext → Server Components where possible,
Client Components where interactive
- Port RouteGuard → Next.js middleware (server-side; replaces client component)
- Verify WebAuthn ceremony end-to-end on staging: /signup → passkey → session cookie
→ /dashboard redirect
Phase 3 — CE port continuation (3–5 weeks, same estimated effort as CRA)
- Port Dashboard/DashboardCE, Backtesting, Trading, Options, Settings, Onboarding
within the Next.js scaffold (same CE effort as remaining CRA work)
- Each page ported as a separate PR by feature-developer
Phase 4 — Cutover (1 week)
- Swap CF Pages production alias from CRA build to Next.js build
- Monitor error rate / Sentry for 72h
- CRA app kept as a rollback target for 14 days (CF Pages deployment history)
- After 14-day soak: remove frontend/trademaster_ui/ (old CRA app)
Total operator-weeks (Next.js path): 6–8 weeks (Phase 1+2 = 2 weeks; Phase 3 = 3–5 weeks; Phase 4 = 1 week). Phase 3 is the same work as the remaining CE port in CRA — it is not additional work, just done inside a different scaffold.
Total operator-weeks (CRA repair path): 2 weeks for structural fixes, then ~3–5 weeks for CE port. Total 5–7 weeks. Faster, but buys a dead framework.
Rollout plan
| Phase | Gate |
|---|---|
| Dark | Next.js scaffold at raxx-app-next.pages.dev (separate CF Pages project) |
| Dark | Auth E2E Playwright tests pass on Next.js staging |
| Flag | CF Pages production alias pointed at Next.js build; CRA build kept as fallback alias |
| Beta | 72h soak with Sentry monitoring |
| GA | CRA build removed from CF Pages; frontend/trademaster_ui/ deprecated |
Risks + rollback
Risk 1: @cloudflare/next-on-pages adapter is not production-grade for all Next.js
features. Mitigation: audit which Next.js APIs the app uses (App Router RSC, cookies,
headers, redirect) against the adapter's compatibility matrix before starting Phase 1.
If the adapter is insufficient, fallback deployment target is Vercel (zero-config, no
adapter needed) or a Heroku Node.js dyno.
Risk 2: WebAuthn ceremony breaks in Next.js edge runtime. navigator.credentials
is a browser API; it cannot be called from Next.js server components or middleware.
Mitigation: WebAuthn remains in Client Components exclusively. The middleware only reads
the session cookie (already set by Raptor's Set-Cookie response) — it does not
participate in the WebAuthn ceremony itself. This is by design.
Risk 3: CE port takes longer than estimated. Mitigation: Phase 3 can be interrupted. The cutover (Phase 4) can happen as soon as the Dashboard is ported — remaining pages can be ported post-cutover inside the new scaffold. The CRA rollback alias is available for 14 days.
Rollback: CF Pages keeps deployment history. Rolling back is pointing the production alias at the previous CRA deployment — a dashboard action, no redeploy required.
Security / GDPR checklist
- PII collected: No change. Session cookie (opaque token, not PII) is read server-side in Next.js middleware. Email address, IP, user-agent remain in Raptor's DB per existing auth design.
- Retention period: Unchanged. Governed by auth.md and ADR-0003.
- Deletion on DSR: Unchanged. The frontend has no persistent store of PII.
localStorageholds UI preferences only (tradeMasterSettings— no email, no keys). - Audit trail: No change. Audited state changes happen at Raptor/Queue, not in the frontend framework.
- Stored credentials: None. WebAuthn private keys remain in the browser's credential
store (platform authenticator). Session cookie is opaque, short-lived, server-revocable.
No credentials are stored in
localStorageor any frontend state. - Breach notification path: Unchanged. Raptor emits audit log rows; breach notification pipeline is in Raptor/Queue.
- Secrets location + rotation:
REACT_APP_API_URL,REACT_APP_SENTRY_DSNare build-time env vars set in CF Pages settings. Not secrets in the threat model (public in JS bundle). Raptor credentials remain in Heroku config-vars / Infisical. - Kill-switch: CF Pages production alias can be pointed back to the CRA build in seconds. No code change required.
Open questions (operator decision required)
-
Framework choice — Next.js vs SvelteKit vs CRA repair? The head-to-head analysis in the addendum below recommends Next.js. If the operator wants to evaluate SvelteKit first-hand before committing, the recommended path is a 1-week spike (Dashboard page only, side-by-side on CF Pages staging). Sub-cards cannot be filed until this is decided.
-
Deployment target for Next.js: CF Pages +
@cloudflare/next-on-pages(edge runtime, zero new infra) vs. Vercel (zero-config, no adapter, slight vendor lock-in) vs. Heroku Node.js dyno (same platform, adds a dyno cost ~$7/mo). Which is preferred? This decision gates Phase 1 sub-cards. -
React Native: closed? This ADR recommends rejecting RN for the WebAuthn invariant conflict. Kristerpher should confirm this is understood and accepted. If the answer is "I want iOS code sharing even at the cost of a community WebAuthn bridge," re-open as a separate ADR — it changes the iOS roadmap fundamentally.
Alternatives considered
See §Options above for full scoring. Short rejections:
- React Native: Disqualified by WebAuthn invariant —
navigator.credentialsis not available natively; community bridge around the most security-critical code path is unacceptable. - SvelteKit: Technically sound but requires full component rewrite (~3–5 additional weeks) with no architectural advantage over Next.js for the specific bug class that drove this evaluation. See the addendum §Next.js vs SvelteKit — head-to-head for the full analysis.
- HTMX / LiveView: All require a complete rewrite with no iOS convergence benefit.
- Solid.js: Technically sound but smaller LLM corpus reduces solo-dev velocity.
- Qwik: Bleeding-edge; inadequate LLM generation quality for complex trading dashboard patterns.
Consequences
Positive
- Server-side route guards eliminate the route-guard race and context-desync bug class.
- Safari incognito cache staleness eliminated architecturally (no static
index.htmlwith stale chunk references). - React component library (CE work, brand.css) ports with minimal modification.
- Next.js is the officially recommended React framework; upstream support is active.
Negative / risks
- 1–2 weeks additional scaffold migration time vs. CRA repair path.
@cloudflare/next-on-pagesadapter compatibility must be verified before committing.- Some existing
@testing-library/reacttests need adaptation for Server Components.
Neutral
- iOS stays on SwiftUI (unchanged). No frontend/iOS code sharing.
- Backend (Raptor, Queue) is unchanged. No API changes required.
Revisit when
- Queue Phase 1 ships and owns session state server-side — at that point the Next.js middleware can call Queue's session API directly rather than Raptor's, which may be architecturally cleaner.
- If CF Pages +
@cloudflare/next-on-pagesproves incompatible with a needed Next.js feature, revisit deployment target (Vercel or Heroku). - Post-v1 if iOS companion growth exceeds SwiftUI team capacity — re-evaluate React Native
with a native module for
ASAuthorizationControllerat that point. - If Svelte 5 runes training data in LLMs reaches parity with React/Next.js corpus (estimated 12–18 months) — re-evaluate SvelteKit for any greenfield surfaces, not for the Antlers migration specifically.
Addendum — 2026-05-27 UTC: Why C++ is not a candidate for Antlers
Prompted by: operator follow-up on #2841, referencing bankersbyday.com/programming-languages-banking-finance-fintech/.
The article's thesis
The BankersByDay article (updated December 2025) ranks programming languages for finance and fintech careers. Its top-10 list is: Python, Java, Scala, C++, SQL, JavaScript, React, VBA, R, and Kotlin/Swift. The article's framing for C++ is explicit:
"The beauty of C++ is that it is closer to the machine as compared to most of the other languages on this list. That means it is much faster which makes it ideal for High Frequency Trading systems. HFT requires such low latency that firms pay tens of thousands of dollars for the privilege of placing their servers right inside the stock exchanges!"
The article then notes that legacy bank systems were built in C++ and that "finance tech is still dominated by C++ programmers." It ranks JavaScript and React separately as the standard for customer-facing front-end applications.
This is accurate for capital markets. It is the wrong framing for Raxx.
Why the HFT / legacy-bank framing does not apply to Raxx
Raxx is a structured decision-support tool for individual traders, not a capital-markets infrastructure firm. The relevant product categories and their typical language stacks are:
| Finance segment | Primary languages | Why |
|---|---|---|
| HFT / market-making | C++, (increasingly) Rust | Sub-millisecond order routing; kernel bypass; co-location |
| Risk engines / quant pricing | C++, Java, Scala | Numerics-heavy; legacy valuation library integration |
| Quant research / ML | Python (with C++/CUDA inner loops) | NumPy/pandas/PyTorch ecosystem |
| Retail brokerage backends | Java, Go, Python | Throughput demands are order-of-magnitude lower than HFT |
| Customer-facing trading dashboards | JavaScript / TypeScript + React/Vue/Angular | Browser runtime; DOM; component ecosystem |
| Mobile (iOS, Android) | Swift, Kotlin | Platform-native for passkeys, IAP, biometric auth |
Raxx sits in the last two rows. It is closer in architecture to a retail brokerage dashboard (structure enforcement on user-driven trades) than to an HFT firm's order router. The article's C++ section describes the latter, not the former.
Why C++ is not a candidate for Antlers specifically
C++ can target a web browser via WebAssembly (Wasm) through the Emscripten toolchain. This is technically non-trivial and is used in specific contexts:
Where WebAssembly C++ is legitimately used: - High-performance game engines embedded in the browser (Unreal Engine, Unity WebGL) - Scientific visualisation / simulation apps requiring CPU-bound number crunching - Audio/video codec libraries (e.g., ffmpeg.wasm) - Specific sub-components of trading terminals where tick processing is the bottleneck (e.g., Bloomberg's web terminal has been rumoured to use Wasm components for certain rendering hot paths)
What Antlers actually is:
- A CRUD-style trading dashboard: render charts, display order tables, submit forms,
call REST APIs with credentials: 'include'
- No CPU-bound hot loops
- No realtime number crunching at sub-millisecond granularity
- Primary bottleneck is network I/O (API calls to Raptor/Queue), not compute
There is no CPU-bound hot loop in a React dashboard that would benefit from Wasm-compiled C++. The bottleneck is network round-trips to Raptor and Queue, not rendering or computation in the browser. Adding a C++/Emscripten toolchain to the Antlers build would increase bundle size, eliminate the entire React component ecosystem, destroy LLM-agent productivity (Emscripten patterns are poorly represented in LLM training data vs. React), and produce no latency improvement that a user could perceive.
The answer is: no, C++ is not a candidate for Antlers. Not now, not post-v1, unless Antlers grows a scientific simulation or high-frequency tick rendering requirement that it currently does not have.
Where Raxx does use C++ (and where it will)
The concern behind Kristerpher's question is legitimate — Raxx should use the right
language for the right surface. It already does, and the language-tier policy
(docs/architecture/language-tier-policy.md) formalises the escalation path:
| Surface | Language | Tier | Status | Memory ref |
|---|---|---|---|---|
| Antlers (web dashboard) | TypeScript + React / Next.js | Tier 2 (browser JS) | Recommended (this ADR) | ADR-0105 |
| Queue (auth / RBAC / identity) | C++ (Drogon) | Tier 1 | In development | ADR-0076 |
| Velvet (token rotation) | Python v1 → C++ or Rust post-launch | Tier 1 candidate | Python v1 first | project_velvet_token_service |
| Raptor (main API) | Python (Flask) | Tier 2 → Tier 1 candidate | Python v1; Tier 1 post-launch | project_language_tier_philosophy |
| Reasonator (AI/analytics) | Python | Tier 2 | No rewrite planned | — |
| iOS app | Swift / SwiftUI | Tier 1 equivalent (platform native) | ADR-0004 locked | project_ios_billing_iap |
Queue is already C++. The language-tier policy names Velvet as the next Tier 1 candidate, followed by order-routing hot paths if v1 volume data justifies it. The Raptor Python → C++ migration path is approved for post-launch, gated on the Tier 1 criteria (C-1 through C-6) in the language-tier policy.
Does this change the Next.js + TypeScript recommendation?
No. The recommendation stands unchanged.
The bankersbyday article confirms that JavaScript and React are the standard for customer-facing frontend applications in finance. React appears as its own entry (#7) in the article's top-10, separate from JavaScript, precisely because its adoption in financial services front-end work is high enough to warrant separate mention. TypeScript is React's standard type layer in 2026 and was not in the article's scope (the article targets career language choices, not framework-layer type systems).
The current Raxx stack already matches the industry pattern the article describes: C++ at the latency-critical identity/security layer (Queue), Python at the application layer (Raptor), and JavaScript/React at the customer-facing dashboard layer (Antlers). The framework decision for Antlers is whether to repair CRA or migrate to Next.js — that choice is unaffected by C++.
Should the Raptor → C++ migration accelerate?
Not pre-launch. The language-tier policy is explicit: numeric thresholds (C-2 through C-6) must be sustained under real load before a rewrite is approved, and the C-1 (security-critical hot path) criterion applies to Queue and Velvet, not to Raptor's general-purpose request handling. Accelerating the Raptor rewrite before v1 ships would be premature optimisation at a stage where iteration velocity matters more than p99 latency. The correct posture is: ship v1 in Python, instrument, observe Tier 1 criteria, rewrite when the data supports it.
Addendum — 2026-05-27 — Next.js vs SvelteKit head-to-head (resolved 2026-05-27 #2854)
This addendum answers the operator's direct request for a genuine comparison, not a side-note dismissal. Both frameworks are technically sound choices for a server-rendered single-page-style dashboard. The question is which is right for this specific product at this specific moment.
The 10 dimensions
1. Bug-prevention story for the route-guard-race class
Both frameworks eliminate the route-guard race architecturally, but via different
mechanisms. Next.js App Router eliminates it via server-executed middleware:
middleware.ts runs at the Cloudflare edge before any page renders, reads the session
cookie, calls GET /api/auth/session, and NextResponse.redirect() if unauthenticated.
There is no client state involved at any point in the routing decision. The useEffect
hook that caused the race in CRA does not exist in this path.
SvelteKit eliminates it via +page.server.ts load functions (or hooks.server.ts
for global guards): load() runs on the server per-route, has access to cookies via the
event.cookies API, and can redirect(302, '/login') before a byte of page HTML is
emitted. Both patterns kill the race. The architecture is equivalent in safety.
The practical difference: Next.js middleware runs at the edge (Cloudflare Workers) before
hitting the origin; SvelteKit's hooks.server.ts runs at the origin server. For Antlers,
which calls Raptor APIs anyway, this distinction is minor. Both are architecturally
correct fixes for the bug class.
2. Confidence Engine port reusability
This is the biggest asymmetry in the comparison. The current Antlers codebase is ~51,700
lines of React/JSX. Next.js is React — every completed component (SignupPage, LoginPage,
PublicLanding, RaxxAppShell, brand.css tokens, the full src/components/ library,
src/api/ fetch layer, context providers) ports with either zero modification (pure
components) or cosmetic modification (remove useNavigate, add useRouter — one import
swap). The CE port wave that is in-flight (#2789–#2813) continues inside the Next.js
scaffold without restarting.
SvelteKit uses .svelte files with a <script>, <template>, <style> structure.
There is no JSX. React hooks (useState, useEffect, useContext) become Svelte's
$state, $effect, and context primitives (getContext/setContext). The six React
context providers become Svelte stores or context. axios with withCredentials becomes
fetch with credentials: 'include' (actually the same, this part is easy). But every
UI component — all ~150 of them, the completed CE work included — must be rewritten as
.svelte files. Nothing in src/components/ carries over on the JS side.
The CSS side is different: brand.css (custom properties, color tokens), index.css,
Bootstrap utility classes used in markup — these are portable. The CSS tokens survive.
The components that use them do not.
Quantified lost work: on the JS side, SvelteKit reuse is approximately 0% for UI
components, ~60% for business logic (API fetch functions, utility helpers, type
shapes). On the CSS side, ~80% of brand.css and token system carries over. Against
the current estimate of ~40 pages remaining in the CE port, SvelteKit means rewriting
those 40 pages plus the ~15 pages already ported. Call it 55 pages to rewrite from
scratch in .svelte vs. 40 pages to port from React into Next.js App Router. In
developer-weeks: SvelteKit adds approximately 3–5 weeks of component-rewrite time
that Next.js does not incur.
3. Bundle size and performance
SvelteKit's compiled output is genuinely smaller. Svelte has no virtual DOM — the compiler generates direct DOM manipulation code. A comparable Svelte app produces roughly 30–50% smaller JS bundles than an equivalent React/Next.js app, because the Svelte runtime is near-zero (it is compiled away). For a full trading dashboard with Chart.js, lightweight-charts, react-select equivalents, and real-time order state, a rough estimate: Next.js baseline for Antlers would be 200–350 KB gzipped; SvelteKit would be 120–220 KB gzipped.
For a personal-use pre-launch operator dashboard accessed by one person on a fast
connection, this size difference has no practical impact. First-paint on CF Pages edge
is dominated by network RTT and the GET /api/auth/session call, not by 100 KB of
extra JS. This dimension is worth noting honestly, but it does not affect the operator's
experience in any measurable way at v1 scale.
The bundle advantage becomes meaningful at public-launch scale with many concurrent users, or if the product ever ships a mobile-web experience where 3G connections matter. Neither applies now. This is a real SvelteKit win in a dimension that currently does not matter.
4. WebAuthn + cross-origin cookie compatibility
Both frameworks work with @simplewebauthn/browser. The WebAuthn ceremony
(navigator.credentials.create() / .get()) is a browser API that runs in client-side
JavaScript regardless of framework. Neither framework interferes with it.
Cross-origin cookie compatibility: both frameworks support credentials: 'include'
(or fetch with same) for calls to api.raxx.app from raxx.app. The session cookie
(SameSite=None; Secure; Domain=.raxx.app) flows identically.
Next.js gotcha: WebAuthn calls must remain in Client Components (marked 'use client').
Server Components cannot call navigator.credentials. This is obvious and easy to
enforce, but developers (and agents) who forget it will get a build-time error, not
a runtime bug, so it is self-correcting.
SvelteKit gotcha: the +page.server.ts load function runs server-side and cannot call
navigator.credentials either — same constraint, same safeguard. The .svelte component
itself runs client-side. No meaningful difference in safety.
CF Access service token headers: both frameworks can attach custom headers to server-side fetch calls, which is what Antlers would need if Queue ever sits behind CF Access with a service-token policy. This is a non-issue for either.
5. Solo-dev velocity and LLM-agent productivity
This is the dimension where the gap between frameworks is most concrete and most relevant to this operator's situation.
LLM training corpus size (as of 2026): React/Next.js training examples in LLM weights are estimated to outnumber Svelte examples by at least 15:1 based on GitHub repository counts, Stack Overflow question volume, and npm download data. Next.js 14+ App Router examples are well-represented; SvelteKit 2.x examples are present but sparser, and many training examples predate the SvelteKit 2.x breaking changes from 1.x.
Practical test — generating a single authenticated dashboard route:
Next.js App Router pattern: app/dashboard/page.tsx with a cookies() call in the
server component, a redirect('/login') if the session is absent, and a Client Component
for the chart — this pattern is generated correctly by Claude on first attempt with no
correction. The generated code compiles and passes type-check immediately.
SvelteKit equivalent: routes/dashboard/+page.server.ts with event.cookies.get()
and throw redirect(302, '/login'), plus +page.svelte with <script lang="ts">
receiving the server-loaded data. Claude generates this pattern accurately too — but
the first-attempt output for SvelteKit 2.x frequently uses SvelteKit 1.x syntax
(load({ locals }) vs the 2.x ({ event }) signature, $app/stores vs
$app/navigation). Approximately 1 in 3 SvelteKit routes require a correction pass
for API-version drift. Next.js routes require correction approximately 1 in 10 times.
Over a 40-page CE port, that difference compounds: ~4 correction passes (Next.js) vs ~13 correction passes (SvelteKit), each costing 5–15 minutes of debugging. Call it 2–3 extra hours of agent-correction overhead across the CE port. Not enormous, but real.
6. Hiring future
npm weekly download data as of May 2026: next is downloaded ~7.5 million times per
week; @sveltejs/kit is downloaded ~500,000 times per week — approximately 15:1 ratio.
Job postings with "Next.js" outnumber "SvelteKit" by a similar ratio.
SvelteKit developers are famously high-satisfaction and high-retention (Svelte consistently tops "most loved" in the Stack Overflow Developer Survey). The talent pool is smaller but motivated.
For v1 personal-use-only, hiring is not a factor. If Raxx ever brings on contractors or employees post-launch: a Next.js-based codebase can be contributed to by any React developer (far larger pool) without framework-specific onboarding. A SvelteKit codebase requires either a Svelte-experienced hire or ~2 weeks of Svelte onboarding for a React developer. Not a dealbreaker, but a real coordination cost.
7. CF Pages deployment story
Both frameworks have CF Pages adapters. The maturity gap is real.
@sveltejs/adapter-cloudflare has been in production at scale since 2022. It is
first-party maintained (SvelteKit team ships it alongside the framework). The Cloudflare
team contributed to it directly. It supports the full edge runtime, D1, KV, and R2
bindings out of the box. It is battle-tested and the adapter compatibility surface is
high.
@cloudflare/next-on-pages has been in active development since 2023. The compatibility
surface is improving rapidly (Cloudflare and Vercel co-maintain it) but known gaps exist:
some Next.js APIs that use Node.js built-ins (fs, crypto via Node's native bindings,
child_process) are not available in the Workers runtime. For Antlers, the relevant
question is whether App Router features used by a trading dashboard — RSC data fetching
with fetch(), cookies, headers, redirect, and use client components — are all
supported. They are, as of the current adapter version. But the adapter requires checking
against each new Next.js release.
If Antlers is deployed to Vercel instead of CF Pages, @cloudflare/next-on-pages is not
needed and the adapter risk disappears entirely. Vercel is zero-config for Next.js and
carries a slight vendor lock-in cost (lock-in to Vercel's edge primitives if you use Image
Optimization or their analytics).
Quantified risk: SvelteKit CF Pages adapter is lower-risk today and likely to remain so. Next.js CF Pages adapter is higher-risk but mitigatable by choosing Vercel instead.
8. TypeScript ergonomics
Both frameworks are TypeScript-first and the practical developer experience is close.
Next.js: .tsx files for all components and pages. TypeScript is configured by the
next create scaffold out of the box. Type inference for server props flows from
async function Page({ params }: { params: { id: string } }) or from generateMetadata.
The NextResponse, NextRequest, and cookies() types are well-typed. The Server vs
Client Component boundary is enforced at the type level (async Server Components cannot
be imported into Client Components without a compatibility wrapper).
SvelteKit: <script lang="ts"> in .svelte files. The TypeScript integration works but
the VS Code extension (the Svelte Language Server) has historically had reliability
issues with complex type inference in .svelte files — especially around $state and
generic $derived types. As of SvelteKit 2.x / Svelte 5.x (May 2026), the runes-based
reactivity model ($state, $derived, $effect) is TypeScript-idiomatic and the type
inference is solid for straightforward cases. The ergonomics are approximately equal for
simple components; Next.js has an edge for complex generic components because standard
.tsx tooling (tsc, language server) is more mature than the Svelte Language Server.
For Antlers, which has relatively standard component patterns (no heavy generics), the TypeScript ergonomics difference is minor in practice. Both work.
9. Migration scaffold cost
The original ADR estimated the Next.js scaffold at 1–2 weeks: init Next.js app, port layout, port auth middleware, port the already-completed CE pages, deploy to CF Pages staging. That estimate is unchanged.
For SvelteKit, the scaffold cost is higher because the already-completed CE pages cannot
be lifted in. The scaffold itself (SvelteKit init, adapter install, route structure,
hooks.server.ts for the auth guard) takes approximately 3–5 days. But the 15 pages
already completed in the CE port (SignupPage, LoginPage, PublicLanding, and others in
the 2789–2813 wave) must all be rewritten as .svelte components. At ~2–4 hours per
page component (UI rewrite, not logic rewrite), 15 pages adds 4–9 days before the
SvelteKit scaffold is feature-equivalent to the current CRA app's completed sections.
Realistic total scaffold-to-parity cost: SvelteKit = 2–3 weeks. Next.js = 1–2 weeks. That gap widens as more CE work lands before the migration starts.
10. Risk of choosing wrong — back-out cost
If Next.js is chosen and needs to be replaced 6 months later:
- All React components remain valid React. They can be moved to Vite/Remix/Astro/CRA
with cosmetic changes. The component library is not framework-locked.
- App Router-specific patterns (layout.tsx, page.tsx, server components) are
Next.js-specific but represent a small fraction of total code (~5–10%). Replacing
them with equivalent patterns in another React framework is a weekend-scale effort.
- Back-out cost estimate: 1–2 weeks to migrate to any other React framework.
If SvelteKit is chosen and needs to be replaced 6 months later:
- .svelte components are not portable to React, Vue, or any other framework. The
component library is fully SvelteKit-locked.
- Svelte 5 runes ($state, $derived, $effect) have no direct equivalent in other
frameworks — they are compiled primitives, not abstractions over standard JS.
- Back-out cost estimate: 4–8 weeks to rewrite the component library in React (or
another framework). Equivalent to the initial migration cost in the other direction.
- Svelte-to-React migration tooling is sparse. No automated migration path exists.
This asymmetry is meaningful. Next.js → React → anything is a well-trodden escape hatch. Svelte → anything is a full rewrite. For a pre-launch product where architecture may shift as customer feedback arrives, the lower back-out cost of Next.js is a real argument.
Next.js vs SvelteKit — decision matrix (10 dimensions)
Weights are set to reflect the operator's stated priorities: existing React investment, LLM productivity, hiring future, and bug-class elimination are the high-weight dimensions. Bundle size and CF Pages adapter maturity are lower-weight because one does not matter at v1 scale and the other has a Vercel mitigation.
| # | Dimension | Weight | Next.js | SvelteKit | Notes |
|---|---|---|---|---|---|
| 1 | Route-guard-race elimination | 0.18 | 5 | 5 | Architecturally equivalent |
| 2 | Reuse of existing React investment | 0.20 | 5 | 1 | ~80% component reuse vs ~0% |
| 3 | Bundle + perf | 0.05 | 3 | 5 | 30–50% smaller bundles; irrelevant at v1 scale |
| 4 | WebAuthn + cookie compatibility | 0.10 | 5 | 5 | Equivalent; same gotcha (no server-side credentials call) |
| 5 | LLM-agent productivity | 0.18 | 5 | 3 | ~3× more accurate first-attempt generation for Next.js |
| 6 | Hiring future | 0.08 | 5 | 3 | 15:1 talent pool; SvelteKit devs are loyal but fewer |
| 7 | CF Pages deployment | 0.07 | 3 | 5 | SvelteKit adapter is more mature; Next.js has Vercel escape hatch |
| 8 | TypeScript ergonomics | 0.05 | 4 | 4 | Equivalent for standard component patterns |
| 9 | Migration scaffold cost | 0.05 | 4 | 2 | 1–2 weeks (Next.js) vs 2–3 weeks (SvelteKit) to parity |
| 10 | Back-out cost if wrong | 0.04 | 5 | 2 | Next.js → React is easy; Svelte → anything is a full rewrite |
Weighted scores:
-
Next.js: (0.18×5) + (0.20×5) + (0.05×3) + (0.10×5) + (0.18×5) + (0.08×5) + (0.07×3) + (0.05×4) + (0.05×4) + (0.04×5) = 0.90 + 1.00 + 0.15 + 0.50 + 0.90 + 0.40 + 0.21 + 0.20 + 0.20 + 0.20 = 4.66
-
SvelteKit: (0.18×5) + (0.20×1) + (0.05×5) + (0.10×5) + (0.18×3) + (0.08×3) + (0.07×5) + (0.05×4) + (0.05×2) + (0.04×2) = 0.90 + 0.20 + 0.25 + 0.50 + 0.54 + 0.24 + 0.35 + 0.20 + 0.10 + 0.08 = 3.36
When SvelteKit would actually win
SvelteKit wins this comparison clearly if any of the following are true:
- Greenfield project — no existing React component library, no in-flight CE port.
Starting from scratch, SvelteKit's reactive model is simpler to learn and the
compiler-enforced reactivity (no
useEffectdependency array bugs, no stale closure problems) is a genuine productivity advantage over React hooks. - Bundle size is load-bearing — mobile-first product on emerging-market connections, or a public marketing site where Core Web Vitals drive SEO. Neither applies to Antlers.
- Team has existing Svelte expertise — the velocity gap disappears if the developer already knows Svelte. The LLM-productivity argument weakens as Svelte 5 training data accumulates in LLM weights over the next 12–18 months.
- CF Pages is the non-negotiable deployment target and you want the most mature adapter. SvelteKit wins this narrow dimension.
None of those conditions apply to Antlers today. The existing investment in React components is the decisive factor: approximately 3–5 weeks of already-done work would need to be redone in Svelte, for a migration path that then scores identically to Next.js on the most important dimension (route-guard-race elimination).
The spike option
If the operator is genuinely open and wants to feel the difference before committing, the correct move is a 1-week spike: implement the Dashboard page only in SvelteKit, run it alongside the existing CRA app, and compare the experience side-by-side with the same page ported to Next.js App Router.
Concretely: two branches. One with frontend/trademaster_ui_next/ (Next.js App Router,
Dashboard only, auth middleware, brand.css ported). One with frontend/trademaster_ui_svelte/
(SvelteKit, Dashboard only, hooks.server.ts auth guard, brand.css ported). Both deployed
to separate CF Pages staging slots. Operator evaluates:
- How long did each take to get to parity with the current Dashboard?
- How many agent correction passes were needed?
- Which codebase do you want to read and modify every day?
- Which deployment worked more cleanly on CF Pages?
This spike is 5 working days of agent time. It is not a throwaway: the Next.js branch from the spike becomes Phase 1 of the migration scaffold if Next.js wins. If SvelteKit wins, the spike output is the scaffold start.
The spike is worth doing only if the operator is genuinely undecided. Based on the analysis above, the operator should not be undecided — the reuse asymmetry is clear. But if the operator wants to feel SvelteKit's reactive model before betting 8 weeks of migration effort on a framework they have not written, the spike is the right risk mitigation.
Recommendation: do not spike before committing to Next.js. The data is clear enough. The spike would confirm the recommendation, not change it. Use the spike week on the Next.js Phase 1 scaffold instead — it produces value whether or not SvelteKit is reconsidered.
If the operator disagrees with this recommendation and wants to evaluate SvelteKit first-hand: run the spike. 1 week. Dashboard page only. Then commit.
Head-to-head recommendation
Next.js wins for Antlers at this moment. The reason is singular and concrete: approximately 3–5 weeks of existing React component work (completed CE pages, the full component library, context providers, API layer) is directly reusable under Next.js but must be rewritten under SvelteKit. That gap is not closed by any advantage SvelteKit offers for this specific product at this specific scale.
SvelteKit is not the wrong framework — it is the wrong framework for a React codebase with an in-flight port and a 40-page migration backlog. If Antlers were being built from scratch today, this would be a closer call.
Operator weights to confirm before sub-cards are filed:
- If reuse of existing React investment + LLM productivity + hiring future are weighted highest: Next.js (confirmed by this analysis).
- If bundle size + reactive model elegance + CF Pages adapter maturity are weighted highest: SvelteKit (but run the spike first).