Overview
Stack, directory map, and the rule that decides where a given piece of state belongs.
The interface is a React 19 + Vite trading terminal. It renders three screens — Trade,
Portfolio, and TLP (built, disabled for the MVP) — against the BFF at /v1.
Stack
| Concern | Choice | Why this one |
|---|---|---|
| Framework | React 19.2 | — |
| Router | React Router DOM 7, data router | Loader-level redirects fire before any element mounts: no flash |
| Build | Vite 8 | — |
| Server state | TanStack Query 5 | Party-scoped keys, one retry policy |
| Client state | Zustand 5 | Slim selectors, persist for preferences |
| Charts (price) | TradingView Charting Library | Vendored in-tree, fed by a custom datafeed |
| Charts (portfolio) | @visx/* | Equity area, ring donut, sparklines |
| Styling | Tailwind CSS v4 | Tokens in src/index.css under @theme; no tailwind.config.ts |
| Motion | motion | Spring micro-interactions |
| Numbers | @number-flow/react | Animated tickers |
| Analytics | PostHog | Masked session replay |
Path alias: @/* → src/*.
Commands
npm run dev # vite :3000, proxies /v1 (+ws) → :4000
npm run build # tsc -b && vite build --mode $(node scripts/build-mode.mjs)
npm run test:run # vitest single run — CI + pre-push
npm run knip # dead-code / unused-export checkscripts/build-mode.mjs picks the Vite mode from the deploying branch: next / main →
production (mainnet), everything else including develop and PR previews → staging
(devnet). Cloudflare Pages supplies CF_PAGES_BRANCH; locally use BRANCH=main npm run build.
Directory map
Where does this state go?
The single question that causes the most churn. The answer is a decision tree, not a preference:
Does it come from the BFF? → TanStack Query. Never mirror server data into Zustand.
Should it survive a shared link? → the URL. The active market is /trade/:marketSlug, full
stop — there is no currentSymbol in any store.
Should it survive a reload? → a persist-ed Zustand store (preferences, active-party,
onboarding).
Everything else → a plain Zustand store, or component state if only one subtree cares.
The one deliberate exception is stores/pending-tpsl: TP/SL levels chosen on a LIMIT order that
has not filled yet. It is not persisted, because it is an intent, not state — a reload
drops it, and the toast says so.
Current state
- Trade — wired to the BFF: pair strip, TradingView chart, live order book, order form with per-market grid validation and TP/SL, positions panel. Market is URL-driven; every read is market-scoped.
- Portfolio — positions, PnL, balances, equity chart, transactions.
- Wallet — exclusive Canton / EVM / Auth0. Placement is wired; cancel and leverage are not.
- TLP — built, route and nav tab both gated off for the MVP.