Tech Stack — Web Frontend
Parent: Tech Stack Overview
Applies to all three Next.js portals: apps/dashboard (:3000), apps/dm (:3002), apps/manage (:3001).
The knowledgebase (apps/knowledgebase :3004) uses Nextra — documented separately at the end.
Framework
| Technology | Version | Notes |
|---|---|---|
| Next.js App Router | v15.3 | Server Components, Server Actions, streaming RSC |
| React | v19.1 | use() hook, concurrent features, form actions |
| TypeScript | v5.x strict | Shared with backend via @leadmetrics/common |
Dev server: next dev (dashboard, dm) / next dev --turbopack (manage).
Server Components are used for:
- All list and detail pages — data fetched directly from Prisma, never via
fetch()to the Fastify API - Session resolution from cookies (never leaks cross-tenant)
- Page-level auth guards:
requireSession()/requireAuth()/requireSuperAdmin()from@/lib/server-auth
Server Actions are used for all mutations (create, update, delete, save wizard steps). No API route mutations in page flows.
Client Components are used for:
- Real-time SSE feeds (live agent output, activity status)
- Socket.IO chat and notification feeds
- Interactive forms and multi-step wizards
- Charts and dashboards (Recharts)
- Calendar views (react-big-calendar)
Styling
| Technology | Version | Notes |
|---|---|---|
| Tailwind CSS | v4.1 | Utility-first; @tailwindcss/postcss plugin |
| tailwind-merge | v3.3 | Conflict-free class merging |
| clsx | v2.1 | Conditional class names |
| class-variance-authority | v0.7 | Variant-based component styles (dashboard only) |
| next-themes | v0.4 | Dark mode — dark: class strategy |
No external component library (no shadcn/ui, no MUI). All UI is custom Tailwind. Radix primitives are not used.
Dark mode rule: every bg-*, border-*, and text-* class requires a paired dark: counterpart.
Responsive breakpoints: all pages must work at 375 / 768 / 1440px. Topbar extras (hidden sm:flex), sidebars (hidden md:block), table non-critical columns (hidden sm:table-cell).
Icons
| Package | Version | Usage |
|---|---|---|
| lucide-react | v0.513 | General UI icons (all apps) |
| react-icons | v5.5 | Brand / social icons — always use fa6 subpath (e.g. react-icons/fa6); SiBing doesn’t exist, use Globe; FaGoogle is in fa not fa6 |
State Management
There is no global client-side state manager. State is managed with:
- React
useState/useReducer— local component state, wizard steps, form state - Next.js Server Components + Server Actions — the primary data layer; mutations invalidate by navigation /
router.refresh() - React Context — minimal: current tenant ID, sidebar open/closed, socket connection
- URL state — list filters, active tab, calendar period (via
useSearchParams)
No TanStack Query. No Zustand. No Redux.
Forms & Validation
| Technology | Version | Notes |
|---|---|---|
| React Hook Form | v7.56 | Uncontrolled, performant forms (dashboard, mobile) |
| @hookform/resolvers | v3.10 | Zod resolver |
| Zod | v3.24–3.25 | Shared schemas — same definition validated client + server |
DM portal uses lighter patterns (controlled inputs + server actions) without react-hook-form.
Data Visualization
| Technology | Version | Notes |
|---|---|---|
| Recharts | v3.8 | Bar charts, line charts, area charts — agent run stats, goal tracking, AI visibility, credits |
All charts are responsive, use Tailwind color tokens, and have dark mode variants.
Calendar
| Technology | Version | Notes |
|---|---|---|
| react-big-calendar | v1 | Shared via packages/ui as ContentCalendar wrapper — blogs, social posts, newsletters |
The shared ContentCalendar component handles tile colors, date chains, and the allItems pattern. Both dashboard and DM portal import from @leadmetrics/ui.
Markdown
| Technology | Version | Notes |
|---|---|---|
| react-markdown | v10.1 | Rendering strategy/context/blog content in UI |
| remark-gfm | v4 | GFM tables, strikethrough, task lists |
| marked | v18 | Markdown → HTML string for PDF generation (via window.open + print) |
The shared MarkdownRenderer component lives in packages/ui — never re-inline it.
PDF Generation
No headless browser (no Puppeteer). Pattern: marked converts Markdown → HTML, injected into a new window with print CSS, then window.print() triggers the browser’s native PDF dialog.
Used in 6 locations: dashboard + DM × (strategy, context, reports).
Real-Time
| Technology | Package | Purpose |
|---|---|---|
| Socket.IO client | socket.io-client v4.8 | Peer-to-peer chat (all 3 portals), real-time notification dropdown (dashboard) |
| EventSource (browser native) | — | SSE for agent output streaming and live activity status |
Socket.IO connects to the Fastify API (not Next.js API Routes). SSE long-lived connections also go to Fastify — Next.js serverless functions cannot hold them open.
Search
| Technology | Package | Purpose |
|---|---|---|
| Typesense | @leadmetrics/provider-typesense | Global Ctrl+K search modal (in packages/ui) — tenant-scoped, 13 collections |
| Fuse.js | fuse.js v7.3 (in packages/ui) | Help Center in-page fuzzy search — no server round-trip |
Dates
| Technology | Version | Notes |
|---|---|---|
| date-fns | v3.6 | Date formatting and arithmetic |
Period date rule: Never new Date(y, m, 1).toISOString() — use ${y}-${mm}-01 string format to avoid UTC+ timezone shifts.
Routing & Navigation
All list pages use infinite scroll (IntersectionObserver) — page-number pagination is banned. List page UX pattern: filters in a left sidebar panel (not toolbar dropdowns); row click navigates to detail (no eye icon button).
Locked screen rule: Never full-screen overlay — sidebar and topbar must remain visible; lock UI only within <main>.
Product Tours
| Technology | Version | Purpose |
|---|---|---|
| driver.js | v1.4 | Guided onboarding tours (all 3 portals) |
Authentication (Web)
Each portal’s Next.js middleware (middleware.ts) uses createJwtAuthMiddleware from @leadmetrics/middleware. Access tokens are httpOnly cookies; expired tokens are silently refreshed via POST /auth/v1/refresh.
// middleware.ts — dashboard example
import { createJwtAuthMiddleware } from "@leadmetrics/middleware";
export const middleware = createJwtAuthMiddleware({
accessTokenCookie: "dashboard_access_token",
refreshTokenCookie: "dashboard_refresh_token",
apiUrl: process.env.API_URL ?? "http://localhost:3003",
publicPaths: ["/login", "/signup", "/forgot-password", "/reset-password"],
});
// export const config must be an inline literal — importing it breaks production build
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };Server components and actions call requireSession() / requireAuth() from @/lib/server-auth — these read the cookie and redirect on failure. requireSession() returns { payload, user }. getSession() returns { user } | null.
Help System
| Technology | Package | Purpose |
|---|---|---|
| HelpTrigger | @/components/help/HelpTrigger | Contextual help drawer — dashboard only. Use <HelpTrigger slug="..." /> — no Link or HelpCircle needed |
| Fuse.js | packages/ui | Help Center search |
DM and Manage portals still use the old <Link> pattern for help.
App-Specific Notes
Dashboard (apps/dashboard — :3000)
- Client portal: tenants review context, strategy, goals, content, channels
- JWT:
jsonwebtokenv9 used directly in some server components/actions - Real-time: Socket.IO notification dropdown (SSE for agent output)
- Onboarding wizard at
/onboarding— 6 steps with brand asset auto-prefill from website crawl - Auth:
requireSession()/requireAuth()from@/lib/server-auth
DM Portal (apps/dm — :3002)
- DM team portal: content review, approval, pipeline management, read-only for context/strategy
- JWT:
josev6 (edge-compatible) - Auth: mandatory
tenantIdgate —TenantMemberis the source of truth (notUser.tenantId) - Proxy routes for all mutations (DM cannot call Prisma directly for cross-tenant mutations)
- No inline editing; read-only access to client-approved items
activeTenantIdprop required on notification dropdown
Manage (apps/manage — :3001)
- Super-admin portal: tenant CRUD, plan management, agent config, system settings, analytics
- JWT:
josev6 (edge-compatible) - Turbopack enabled in dev
better-authpackage present in dependencies (legacy — JWT middleware is active auth path)- 19-tab vertical sidebar on tenant detail; infinite scroll for Keywords, Reports, etc.
- Manage System group: RAG & AI Config + Deliverable Settings
Knowledgebase (apps/knowledgebase — :3004)
| Technology | Version | Notes |
|---|---|---|
| Nextra | v4 | MDX-based docs site built on Next.js 15 |
| nextra-theme-docs | v4 | Docs theme with sidebar navigation |
| mermaid | v11 | Architecture + flow diagrams in MDX |
Content lives in docs/ (junction to this monorepo’s docs/ directory). Navigation defined in _meta.ts files.
Shared Packages Used by Web Apps
| Package | What it provides |
|---|---|
@leadmetrics/db | db Prisma client — direct in Server Components, never via fetch |
@leadmetrics/ui | MarkdownRenderer, ContentCalendar, GlobalSearch (Ctrl+K), HelpCenter |
@leadmetrics/common | formatDate, formatCurrency, badge color helpers, apiFetch |
@leadmetrics/middleware | createJwtAuthMiddleware |
@leadmetrics/nosqldb | writeAuditLog — auto-connects MongoDB |
@leadmetrics/queue | enqueue* helpers for server actions |
@leadmetrics/storage | uploadToSpaces, presigned URL helpers |
@leadmetrics/provider-typesense | Typesense client for search |
Testing
| Layer | Technology | Version | Scope |
|---|---|---|---|
| Unit | Vitest | v2.1 | Server actions, utilities, prompt builders |
| Integration | Vitest | v2.1 | API route handlers with real test DB |
| E2E | Playwright Test | v1.59 | Full portal journeys |
| Component | @testing-library/react | v16.3 (dm only) | Isolated component tests |
E2E setup: global-setup.ts pre-warms /api/auth/sign-in/email before login (lazy-compile timeout prevention). Test timeout: 300s. Stale dev server on port 3008 with wrong DB silently breaks login — check if anything is running there.
Playwright patterns:
- Sticky footer buttons: use JS
.click()or direct fetch, not standard Playwright click - Hover-reveal buttons (
opacity-0): wait → hover → scoped click → close-as-save-indicator - Avoid
.or()(strict violation);getByTextneeds{ exact: true };waitForURLaborts on SPA — usewaitForFunction - Infinite scroll: trigger via IntersectionObserver, not page-number clicks
Environment Variables (Web Apps)
# API
API_URL=http://localhost:3003 # used by middleware for token refresh
NEXT_PUBLIC_API_URL=http://localhost:3003
# Auth (JWT — shared with API and all portals)
JWT_SECRET=
REFRESH_TOKEN_SECRET=
# Internal API secret
INTERNAL_API_SECRET=
# Email dev filter
DEV_ALLOWED_EMAIL_DOMAINS=leadmetrics.ai