Client Portal — Architecture
The client-facing Next.js portal — App Router structure, the eleven-deep provider stack, the hand-written axios data layer (and the orphaned SWR template leftovers), MUI 7 + Aurora theming with the Stack-row-default gotcha, i18n/OTel, and the build/deploy reality (standalone Node under systemd + nginx — NOT Cloudflare Pages, contradicting the in-repo ARCHITECTURE.md).
The client portal (frontend/client/, package octopus-client) is the customer-facing dashboard where clients manage orders, wallet, payouts, catalog, and account settings. It's a Next.js 15.5 / React 19 App Router app (all pages CSR, 'use client') that talks to the Go backend's /client/* routes over httpOnly-cookie auth. This page is the architecture hub; see Client Portal → Auth and Client Portal → Settings for the deep dives, and Frontends for where it sits among the surfaces.
Built on the Aurora (Prium) MUI template
Much of the scaffolding — theme, config.ts (assetsDir defaults to prium.github.io/aurora), the services/swr/* layer — is Aurora template code. Real data flows through a hand-written axios service; several template pieces are dead leftovers (flagged below).
Routing
File-based App Router with two route groups:
(auth)— public:login,forgot-password,reset-password.(main)— everything gated:orders,top-up,esim,products,cart,transactions,payouts,scheduled-payouts,beneficiaries,exports,settings, plus[id]detail routes.
(main)/layout.tsx is a server component forcing export const dynamic = 'force-dynamic', wrapping children in <AuthGuard><MainLayout>. There is no Next.js middleware.ts — route protection is 100% client-side (see Auth).
Provider stack
Eleven providers nest in app/layout.tsx (outer → inner):
i18n and telemetry are side-effect imports in layout.tsx, not providers. EcommerceProvider/BulkSelectProvider exist but are mounted per-feature, not at the root.
Data layer
Primary: one hand-written axios client — src/services/api/client.ts (~1150 lines). A single instance, baseURL = ${NEXT_PUBLIC_API_URL||'http://localhost:8081'}/client, withCredentials: true (cookies). It exports ~22 typed API objects (authApi, twoFactorApi, apiKeysApi, webhooksApi, g2aApi, ipWhitelistApi, ordersApi, walletsApi, topupsApi, esimApi, …). A response interceptor does the 401→refresh→replay. extractData unwraps the {success, data, error} envelope and throws error.message on failure.
The SWR layer is orphaned template code
The global SWRConfiguration wrapper is defined but never mounted in the provider tree, so its settings never apply. The only SWR usage (useProductApi.ts) uses a dummyFetcher (the axiosFetcher import is commented out). Treat services/swr/* as Aurora leftovers — real data goes through the axios service objects. Also note topupsApi/esimApi are typed with any throughout (added quickly), unlike the older strongly-typed sections.
Theming, i18n, telemetry
- MUI 7 (
@mui/material7.3,@mui/lab7 beta,x-data-grid8,x-date-pickers8) +@mui/material-nextjsApp Router adapter. Theme built intheme/theme.tsviacreateTheme(textDirection, locale); ~50 component overrides + anoctopus-cardsbrand palette alongside the default Aurora one.
The Stack-row-default gotcha
theme/components/Stack.tsx overrides MuiStack defaultProps to { useFlexGap: true, direction: 'row' }. A bare <Stack> lays out horizontally, the opposite of stock MUI (which defaults to column). Every vertical stack in the app must set direction="column" explicitly — a frequent source of layout surprises. See the memory note.
- i18n —
i18nextwith 6 locales (en, fr, bn, zh, hi, ar), defaultenUS, RTL viatheme/RTLMode.tsx. But the auth screens are hardcoded English (no i18n) — a gap if localization ships. - OpenTelemetry (web) —
lib/telemetry.ts, browser-only, gated onNEXT_PUBLIC_OTEL_ENABLED==='true'.WebTracerProvider+BatchSpanProcessorauto-instruments DocumentLoad + Fetch, OTLP-HTTP to SigNoz, and propagatestraceparentto the API (propagateTraceHeaderCorsUrls: [API_URL]) so client spans join the backend trace. (The default endpoint literal istelemetry.octopusrewards.com— note the domain differs from the usualoctopuscards.io.)
Build & deploy
Deploy reality contradicts the in-repo ARCHITECTURE.md
next.config.ts sets output: 'standalone' with a comment explicitly forbidding output: 'export' (static export can't serve dynamic [id] routes). The app is deployed as a standalone Node server under systemd, exactly like the Go backend — not Cloudflare Pages. The 2894-line frontend/client/ARCHITECTURE.md still describes the old Cloudflare-Pages static-export plan (wrangler pages deploy out, output: 'export', _redirects/_headers), and a stale out/ directory lingers on disk. Trust the config + the deploy/ scripts, not ARCHITECTURE.md.
The live deploy (mirrors the backend runbook):
- systemd unit
deploy/octopus-client.service—User=octopus,WorkingDirectory=/opt/octopus-client,ExecStart=/usr/bin/node /opt/octopus-client/server.js,PORT=3001,EnvironmentFile=/etc/octopus/octopus-client.env. Fronted by nginx (deploy/nginx-octopus-client.conf). Sandbox variants exist (octopus-sandbox-client.service, etc.). - Update script
deploy/update-octopus-client.sh:bun install→bun run build→systemctl stop→ copy.next/standalone/.+.next/static+publicinto/opt/octopus-client/→systemctl start→ health check. Build uses bun; runtime is Node.
Environment
| Var | Purpose |
|---|---|
NEXT_PUBLIC_API_URL | backend base; client appends /client (dev default http://localhost:8081) |
NEXT_PUBLIC_ASSET_BASE_URL | optional; defaults to the Aurora GitHub Pages CDN |
NEXT_PUBLIC_BRAND | default | octopus-cards |
NEXT_PUBLIC_OTEL_ENABLED / _OTEL_ENDPOINT / _SERVICE_NAME / _ENVIRONMENT, NEXT_PUBLIC_APP_NAME | used in code but absent from .env.example (doc gap) |
No committed secrets — .env (untracked) holds only NEXT_PUBLIC_APP_NAME; .env.example is placeholder URLs; the OTel access-token header is left commented out. All public config is NEXT_PUBLIC_* (client-visible by design).
Findings
| Finding | Detail |
|---|---|
| ARCHITECTURE.md stale | documents Cloudflare Pages static export; reality is standalone/systemd (highest-priority doc fix) |
Stale out/ dir | leftover static-export artifact with CF _redirects/_headers; not produced by the current build |
| Orphaned SWR layer | SWRConfiguration never mounted; useProductApi uses a dummyFetcher |
| Stack row-default | global override flips MUI Stack to horizontal |
| Auth screens un-i18n'd | hardcoded English despite 6 locales elsewhere |
any-typed newer APIs | topupsApi/esimApi loosely typed |
| OTel domain oddity | default endpoint telemetry.octopusrewards.com, not octopuscards.io |
Key files
- Entry/providers:
src/app/layout.tsx,src/providers/* - Data:
src/services/api/client.ts; configsrc/config.ts,src/routes/* - Theme:
src/theme/theme.ts,src/theme/components/Stack.tsx; i18nsrc/locales/i18n.ts; OTelsrc/lib/telemetry.ts - Build/deploy:
next.config.ts,deploy/octopus-client.service,deploy/update-octopus-client.sh,deploy/nginx-octopus-client.conf; staleARCHITECTURE.md
Frontend Apps
Every frontend surface — the claim worker, client portal, marketing site, docs, this wiki, the mock API — plus the server-rendered admin UI.
Client Portal — Auth & 2FA
The client portal's authentication end-to-end — the httpOnly-cookie session model, the login + TOTP-2FA + passkey flows with a sequence diagram, the axios 401→refresh single-flight, client-side route guards, forgot/reset password, logout, the full endpoint map, and the findings (no CSRF token, no schema validation on login, doc-vs-code drift).