Admin Architecture
Purpose
Deep-dive into apps/admin — the Next.js 14 enterprise dashboard
used by counselors, marketing staff, and admins. Start here if your
task touches pages, layout, components, client-side realtime, or
the NextAuth session flow.
Prerequisites
- Architecture overview
- Setup complete, admin running
- Familiarity with Next.js 14 App Router
Tech stack
- Next.js 14 — App Router, React Server Components
- React 18
- Tailwind CSS — with design tokens in
tailwind.config.ts - shadcn/ui (new-york style) — primary component library
- Recharts — analytics charts
- NextAuth (JWE) — auth
- Socket.io client — realtime events
- SWR (where used) or polling — data fetching
Process and hosting
- Dev port: 3103 (
npm run dev:admin) - Prod: systemd service
huph-admin.service, port 47293 - Nginx:
admin.huph.val.idproxies to127.0.0.1:47293 - Restart prod:
sudo systemctl restart huph-admin
Enterprise shell pattern
The root authenticated layout uses a fixed-shell pattern:
sidebar and header stay put while content scrolls. This is
implemented in src/components/layout/authenticated-layout.tsx:
┌──────────────────────────────────────────────┐
│ AppHeader (fixed, never scrolls) │
├────────┬─────────────────────────────────────┤
│ Sidebar│ Main content area │
│ (fixed)│ (overflow-y-auto, scrolls) │
│ │ │
│ │ │
└────────┴─────────────────────────────────────┘
Critical rule (from feedback_enterprise_shell_pattern): the
root must use h-screen overflow-hidden, NOT min-h-screen. The
column wrapper is overflow-hidden and the main pane is
flex-1 overflow-y-auto overflow-x-hidden.
Using min-h-screen re-breaks the scroll — the body grows as
content grows, pushing sidebar and header off-screen. This was a
known bug, root-caused and fixed 2026-04-08. DO NOT regress.
Research-backed: Gmail, Slack, Linear, Notion, Intercom, and Salesforce Lightning all use this flex-based fixed shell.
Key pages
| Path | Purpose |
|---|---|
/ |
Overview — today's stats, pending escalations |
/conversations |
3-pane inbox (list + detail + chat thread) |
/conversations/[id] |
Single conversation detail |
/conversations/inbox |
Alternative inbox view (older UX) |
/conversations/pipeline |
Conversations grouped by lead stage |
/leads-v2 |
Lead list with cluster + stage filters, 30s polling |
/leads-v2/[id] |
Lead detail with Reassign + status actions |
/analytics-v2 |
13 charts dashboard |
/knowledge/* |
KB documents, sources, eval, gaps, FAQ |
/follow-up |
Follow-up queue, rules, stats tabs |
/counselor-dashboard |
Per-counselor view (Phase 1.6) |
/settings/* |
Chatbot settings, escalation rules, users, clusters |
Auth flow
- User opens
admin.huph.val.id - nginx forwards to Next.js (port 47293)
- Middleware checks NextAuth session via JWE cookie
- Unauthenticated → redirect to
/signin - Signin form posts credentials to
/api/auth/callback/credentials - NextAuth verifies against
admin_userstable (bcrypt hash) - JWE session cookie issued, user redirected to requested page
Session cookie expires after 24 hours.
API proxy pattern
All admin API calls go through Next.js Route Handlers at
src/app/api/*. These proxy to apps/api via the shared
serverFetch helper in src/lib/serverFetch.ts:
Browser → Next.js /api/leads-v2 (Route Handler)
→ serverFetch (injects X-Internal-Secret + X-Forwarded-Session JWE)
→ apps/api /api/v1/leads/v2 (gated by middleware)
Critical: serverFetch is the ONLY way admin should talk to
the API. Direct fetch() bypasses auth injection and will 401
under API_AUTH_MODE=enforce. 21 proxy routes were migrated to
serverFetch in commit e3ec319.
Realtime via Socket.io
src/lib/socket.tsx provides a SocketProvider React context that
wraps the authenticated layout. Pages subscribe via the
useSocketEvent hook in src/hooks/useSocketEvent.ts:
useSocketEvent('message.new', (payload) => {
// payload includes conversation_id, cluster_id, etc
if (payload.conversation_id === currentConvId) {
addMessageToThread(payload);
}
});
Socket connection authenticated with NextAuth JWE (via
X-Forwarded-Session header on the upgrade request).
Room scoping: counselor in cluster A never receives events for
cluster B leads. Enforced server-side in
apps/api/src/realtime/rooms.ts resolveRoomsForUser.
Health indicator: ConnectionStatus component shows
Connected/Connecting/Offline in the header. Offline typically
means NEXTAUTH_SECRET missing from API container env.
RBAC (Phase 1.5)
Cluster-based access control implemented at the admin proxy + Prisma route layer. Rules:
marketing_counselorwithcluster_id = X→ sees only cluster X leads and conversationsmarketing_staffandmarketing_admin→ see all (global)admin→ see all, can reassign clusters
Filter applied in:
- /admin/leads-v2 API proxy (list endpoint)
- /admin/analytics-v2 API proxy
- /admin/counselor-dashboard API proxy
- Prisma routes (where used)
Spec: docs/superpowers/specs/2026-04-08-rbac-phase15-design.md.
Shared components
Component library in src/components/:
ui/— shadcn/ui primitives (Button, Card, Dialog, Tabs, etc.)shared/—StatCard,ChartCard,EmptyState,ConnectionStatus,Skeleton,ConfirmDialoglayout/—AppHeader,AppSidebar,authenticated-layoutleads/—LeadCard,LeadFilterChips,ClusterBadgeconversations/—ThreadList,ChatThread,ReplyBox,FeedbackActions,CorrectionPanel
No test infrastructure
The admin app has no Jest/Vitest/Playwright setup. Verification
happens via npx tsc --noEmit, npm run build, and manual browser
smoke. See the feedback_admin_no_test_infra memory and the
running-tests page for the full
rationale.
Gotchas
- Enterprise shell must use
h-screen, notmin-h-screen. See the pattern description above — reverting re-breaks sidebar and header scroll. apps/webchat-enterpriseused to conflict on port 3103. Deleted in Apr 8 cleanup. If you see port conflicts, check for a stray process, not the old webchat.serverFetchis mandatory for API calls. Directfetch()skips auth injection and breaks underAPI_AUTH_MODE=enforce.- Dev port 3103 vs prod 47293. Don't hardcode — use env vars.
- **
module-level throws breaknext build.** The originalserverFetch.tshad a top-levelthrow` if env missing, which fired during Next.js static analysis. Move env checks inside function bodies. - Dark mode is defined but not activated. CSS vars exist in
tailwind.config.tsbut the theme toggle is not wired up. Low priority follow-up. - Realtime depends on
NEXTAUTH_SECRETin API container env. Missing secret → Socket.io clients can't decode JWE → Offline forever. Add todocker-compose.yml+ root.env. CorrectionPanel+FeedbackActionslabels are in Indonesian, the rest of admin is mostly English. Minor inconsistency, needs product decision.
See also
- Architecture overview
- API — the backend admin talks to
- Running tests — admin no-test rationale
- Admin redesign spec:
docs/superpowers/specs/2026-03-21-enterprise-admin-redesign-design.md