Skip to Content
Tech StackTech Stack — Web Frontend

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

TechnologyVersionNotes
Next.js App Routerv15.3Server Components, Server Actions, streaming RSC
Reactv19.1use() hook, concurrent features, form actions
TypeScriptv5.x strictShared 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

TechnologyVersionNotes
Tailwind CSSv4.1Utility-first; @tailwindcss/postcss plugin
tailwind-mergev3.3Conflict-free class merging
clsxv2.1Conditional class names
class-variance-authorityv0.7Variant-based component styles (dashboard only)
next-themesv0.4Dark 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

PackageVersionUsage
lucide-reactv0.513General UI icons (all apps)
react-iconsv5.5Brand / 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

TechnologyVersionNotes
React Hook Formv7.56Uncontrolled, performant forms (dashboard, mobile)
@hookform/resolversv3.10Zod resolver
Zodv3.24–3.25Shared schemas — same definition validated client + server

DM portal uses lighter patterns (controlled inputs + server actions) without react-hook-form.


Data Visualization

TechnologyVersionNotes
Rechartsv3.8Bar 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

TechnologyVersionNotes
react-big-calendarv1Shared 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

TechnologyVersionNotes
react-markdownv10.1Rendering strategy/context/blog content in UI
remark-gfmv4GFM tables, strikethrough, task lists
markedv18Markdown → 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

TechnologyPackagePurpose
Socket.IO clientsocket.io-client v4.8Peer-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.


TechnologyPackagePurpose
Typesense@leadmetrics/provider-typesenseGlobal Ctrl+K search modal (in packages/ui) — tenant-scoped, 13 collections
Fuse.jsfuse.js v7.3 (in packages/ui)Help Center in-page fuzzy search — no server round-trip

Dates

TechnologyVersionNotes
date-fnsv3.6Date 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

TechnologyVersionPurpose
driver.jsv1.4Guided 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

TechnologyPackagePurpose
HelpTrigger@/components/help/HelpTriggerContextual help drawer — dashboard only. Use <HelpTrigger slug="..." /> — no Link or HelpCircle needed
Fuse.jspackages/uiHelp 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: jsonwebtoken v9 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: jose v6 (edge-compatible)
  • Auth: mandatory tenantId gate — TenantMember is the source of truth (not User.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
  • activeTenantId prop required on notification dropdown

Manage (apps/manage — :3001)

  • Super-admin portal: tenant CRUD, plan management, agent config, system settings, analytics
  • JWT: jose v6 (edge-compatible)
  • Turbopack enabled in dev
  • better-auth package 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)

TechnologyVersionNotes
Nextrav4MDX-based docs site built on Next.js 15
nextra-theme-docsv4Docs theme with sidebar navigation
mermaidv11Architecture + 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

PackageWhat it provides
@leadmetrics/dbdb Prisma client — direct in Server Components, never via fetch
@leadmetrics/uiMarkdownRenderer, ContentCalendar, GlobalSearch (Ctrl+K), HelpCenter
@leadmetrics/commonformatDate, formatCurrency, badge color helpers, apiFetch
@leadmetrics/middlewarecreateJwtAuthMiddleware
@leadmetrics/nosqldbwriteAuditLog — auto-connects MongoDB
@leadmetrics/queueenqueue* helpers for server actions
@leadmetrics/storageuploadToSpaces, presigned URL helpers
@leadmetrics/provider-typesenseTypesense client for search

Testing

LayerTechnologyVersionScope
UnitVitestv2.1Server actions, utilities, prompt builders
IntegrationVitestv2.1API route handlers with real test DB
E2EPlaywright Testv1.59Full portal journeys
Component@testing-library/reactv16.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); getByText needs { exact: true }; waitForURL aborts on SPA — use waitForFunction
  • 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

© 2026 Leadmetrics — Internal use only