5.7 KiB
Architecture
What the app is
SAR Link Portal — a customer + admin portal for an ISP. Users register (verified against the national identity registry), add network devices, and pay for device subscriptions either by bank transfer (MIB) or from a prepaid wallet they top up. Admins verify users, manage devices/payments/topups, and credit wallets.
Stack (current)
- Next.js 15 (App Router, React 19), TypeScript.
- Tailwind CSS v4 (CSS-first, no config file) + shadcn/ui (
new-york, neutral) + Radix + lucide icons. - Jotai (global UI state) + React Query (mounted, not yet used).
- react-hook-form + zod (forms/validation), nuqs (URL query state), sonner (toasts), next-themes, motion (animation).
- Auth: mid-migration — old next-auth (JWT session) → new localStorage token (
lib/auth-store.ts) + axios api-client (lib/api-client.ts).
Folder map
app/
(auth)/auth/{signin,signup,verify-otp,verify-otp-registration}/ # public auth pages
(dashboard)/{devices,payments,top-ups,wallet,users,...}/ # authed app (user + admin)
api/{auth/[...nextauth],check-devices}/ # frontend API routes (being removed)
layout.tsx page.tsx globals.css auth.ts
actions/ # server actions: auth-actions, payment, user-actions, (omada, ninja = dead)
queries/ # server data fetchers: authentication, devices, islands, users, wallet
components/ # feature components + components/ui (shadcn primitives)
lib/ # auth-store, api-client, atoms, backend-types, schemas, person, utils
utils/ # tryCatch, axios-client (dead), axiosInstance (dead)
providers/ # QueryProvider, theme, (AuthProvider = next-auth, being removed)
hooks/ middleware.ts(removed)
docs/ # this documentation
deploy/ # (superseded — real prod is .build/prod/, see below)
Current architecture (BFF — what's being replaced)
Browser ──(RSC + Server Actions)──▶ Next.js server ──(fetch + Token)──▶ Django API
The Next.js server sits between the browser and Django:
- Server components + server actions read the token via
getServerSessionand call Django server-side overSARLINK_API_BASE_URL. - Public origin
/api/*= Next.js's own routes (NextAuth); Django's/apiis reached only internally. - Deployed as a Node container behind nginx (
.build/prod/).
Target architecture (static + nginx, direct-to-API)
Browser ──(static HTML/JS from nginx)
├─ API_URL calls (/api, /callback, /auth/*) ─▶ nginx ─▶ Django
└─ everything else ─▶ nginx serves the static build
Goals set by the project owner:
- No Node/Bun in production — ship a static build served by nginx.
- Configurable API base:
API_URL=http://localhost:8000in dev,API_URL=https://portal.sarlink.net/api(nginx → backend) in prod. - Browser calls the Django API directly (same-origin in prod via nginx; cross-origin in dev with CORS).
Two ways to get there (under discussion):
- Next.js static export (
output: "export") — keeps Next; fights the framework (no server actions,searchParamsneeds Suspense, dynamic routes need query-params, middleware gone). Foundation already built:lib/auth-store.ts,lib/api-client.ts,components/auth/route-guard.tsx, config flip, devrewrites()proxy. See../STATIC_MIGRATION_PLAN.md. - Vite + React SPA (recommended) — natural fit for a static client app with a configurable
API_URL(import.meta.env.VITE_API_URL). A port, not a rewrite: all components, shadcn, react-query, jotai, zod transfer 1:1; only the shell (routing,next/link,next/font,next/image, layouts) changes. TanStack Router or React Router for routing.
Either way the auth/data plumbing already written is reusable: token in localStorage, Authorization: Token interceptor, client route guard, React Query for reads/mutations.
Auth model (target)
- Login:
POST {API}/callback/auth/ { token: pin }→{ token, user, expiry }. - Store token + user in localStorage (
setAuth). - Attach
Authorization: Token <token>on every request (axios interceptor). RouteGuardgates authed pages;401→clearAuth()+ redirect to signin.- Admin gating from
user.is_admin/user.user_permissions.
Production deployment (where nginx lives)
The real prod stack is .build/prod/ (repo root), not frontend/deploy/:
compose.yml— postgres + backend (Django/gunicorn) + frontend + nginx (single entrypoint,:8080→80).nginx.conf— reverse proxy. Currently built for the BFF model (/→ Next Node server;/apiNOT proxied to Django). For the static migration this must change to: serve the static build + proxy/api,/callback,/auth/{login,logout,mobile}to Django; the separate Nodefrontendservice goes away.frontend.Dockerfile— currentlyoutput:"standalone"+node server.js; becomes a static build folded into the nginx image.
See api-endpoints.md for exact paths and the /auth/* frontend-vs-backend collision that the nginx config must handle.
Known issues / cleanup
- Mid-migration auth: dashboard pages/actions/queries still use
getServerSession; they must move to the client token store (or be removed in the SPA port). - Stale wallet balance: header balance comes from the login
userAtomsnapshot; not refreshed after topups/payments until re-login. - Dead code: Omada, Invoice Ninja,
/auth/login/,/islands/,/inventory/,backendMobileLogin,app/api/check-devices,utils/axios-client.ts,utils/axiosInstance.ts. - person-verify is called directly from the frontend; planned to route through the backend.