OctoWiki

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 clientsrc/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/material 7.3, @mui/lab 7 beta, x-data-grid 8, x-date-pickers 8) + @mui/material-nextjs App Router adapter. Theme built in theme/theme.ts via createTheme(textDirection, locale); ~50 component overrides + an octopus-cards brand 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.

  • i18ni18next with 6 locales (en, fr, bn, zh, hi, ar), default enUS, RTL via theme/RTLMode.tsx. But the auth screens are hardcoded English (no i18n) — a gap if localization ships.
  • OpenTelemetry (web)lib/telemetry.ts, browser-only, gated on NEXT_PUBLIC_OTEL_ENABLED==='true'. WebTracerProvider + BatchSpanProcessor auto-instruments DocumentLoad + Fetch, OTLP-HTTP to SigNoz, and propagates traceparent to the API (propagateTraceHeaderCorsUrls: [API_URL]) so client spans join the backend trace. (The default endpoint literal is telemetry.octopusrewards.com — note the domain differs from the usual octopuscards.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.serviceUser=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 installbun run buildsystemctl stop → copy .next/standalone/. + .next/static + public into /opt/octopus-client/systemctl start → health check. Build uses bun; runtime is Node.

Environment

VarPurpose
NEXT_PUBLIC_API_URLbackend base; client appends /client (dev default http://localhost:8081)
NEXT_PUBLIC_ASSET_BASE_URLoptional; defaults to the Aurora GitHub Pages CDN
NEXT_PUBLIC_BRANDdefault | octopus-cards
NEXT_PUBLIC_OTEL_ENABLED / _OTEL_ENDPOINT / _SERVICE_NAME / _ENVIRONMENT, NEXT_PUBLIC_APP_NAMEused 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

FindingDetail
ARCHITECTURE.md staledocuments Cloudflare Pages static export; reality is standalone/systemd (highest-priority doc fix)
Stale out/ dirleftover static-export artifact with CF _redirects/_headers; not produced by the current build
Orphaned SWR layerSWRConfiguration never mounted; useProductApi uses a dummyFetcher
Stack row-defaultglobal override flips MUI Stack to horizontal
Auth screens un-i18n'dhardcoded English despite 6 locales elsewhere
any-typed newer APIstopupsApi/esimApi loosely typed
OTel domain odditydefault endpoint telemetry.octopusrewards.com, not octopuscards.io

Key files

  • Entry/providers: src/app/layout.tsx, src/providers/*
  • Data: src/services/api/client.ts; config src/config.ts, src/routes/*
  • Theme: src/theme/theme.ts, src/theme/components/Stack.tsx; i18n src/locales/i18n.ts; OTel src/lib/telemetry.ts
  • Build/deploy: next.config.ts, deploy/octopus-client.service, deploy/update-octopus-client.sh, deploy/nginx-octopus-client.conf; stale ARCHITECTURE.md

On this page