Documentation
Architecture

Tech Stack

Every dependency we ship and why it is in the bundle.

Runtime & framework

PackageVersionWhy
next16.xApp Router, Server Components, route handlers for /api.
react / react-dom19.xPinned by Next 16.
typescript5.xStrict mode is enabled.
@supabase/ssrlatestCookie-aware Supabase client for SSR + middleware.
@supabase/supabase-jslatestBrowser-side queries.

Data, charts, PDF

PackageUsed for
@tanstack/react-queryCache for some integration fetches. Most metric fetches use plain useEffect + fetch — react-query is opt-in.
chart.js + react-chartjs-2All charts on the dashboard. Components live in src/components/dashboard/.
@react-pdf/rendererServer-rendered PDFs from React components in src/components/reports/.
lucide-reactEvery icon in the app. Do not import from any other icon set.
googleapisOfficial Google client library used by every Google integration (GA4, GSC, GBP, YouTube, Ads).

Styling

Tailwind CSS 4 via @tailwindcss/postcss. We use the new @theme directive in globals.css to define the design tokens. Most colors are CSS variables (--brand, --surface-100, etc.) consumed via Tailwind's arbitrary value syntax (bg-[var(--surface-100)]) and via the named tokens (bg-brand, text-foreground).

Brand & typography

FPM brand: black + orange. Display headings use Montserrat (weights 400 / 500 / 600 / 700 / 800 + 500 italic). Body uses DM Sans. We dropped Fraunces in May 2026 — Montserrat reads cleaner at the small body sizes used in dashboard chrome.

TokenValueUse
--font-display'Montserrat', system-ui, sans-serifh1/h2/h3 + the .font-display utility class.
--font-sans'DM Sans', 'Inter', system-ui, sans-serifBody, paragraphs, MetricCard rows, all default text.
--black#0e0e0eTinted near-black. Default text. HubNav background.
--orange#F26419FPM brand orange. CTAs, current phase, accent rules.
--orange-mid#f7812eHover state for orange surfaces. Tier badge text fallback.
--off-white#F8F7F5Page background — tinted warm, not pure white.
Sidebar bg#202020Slightly warmer than --black so the sidebar reads as chrome, not content.

Two themes live in globals.css: light (default) and dark (toggled by adding the .dark class to <html>). The documentation site you are reading is hard-coded dark; the dashboard defaults to light.

Heading weights
Inline fontWeight: 400 on Fraunces headings was elegant but Montserrat 400 looks slightly underweight at large sizes. We bumped most h1-class displays to fontWeight: 600 with letter-spacing: -0.025em for hero sizes. Sub-headings (15-22px) sit at fontWeight: 500.

State

We deliberately keep state simple. There is no Redux, no Zustand, no Jotai. Everything is local component state or fetched on demand:

  • useState for UI state (active tab, date range, modal open).
  • useEffect + fetch for metric data, keyed on client.id and the date range.
  • @tanstack/react-query only where dedup or background refetch is genuinely needed.

Path alias

@/* maps to ./src/*. Always import via the alias, not relative paths:

ts
// good
import { createClient } from '@/lib/supabase/server'
import ClientDashboard from '@/components/dashboard/ClientDashboard'

// avoid
import { createClient } from '../../../lib/supabase/server'

Lint config

ESLint flat config in eslint.config.mjs. Some rules we have turned off intentionally — TypeScript's any is allowed for dynamic webhook payloads where the shape varies per client. Don't add new any annotations elsewhere; prefer narrow types.

What we deliberately don't use

  • No Prisma / Drizzle. Supabase JS client only.
  • No tRPC / GraphQL. Plain REST under /api/*.
  • No CSS-in-JS. Tailwind utilities and the occasional CSS variable.
  • No date library. Native Date and toISOString().slice(0, 10). If date math gets gnarly we will add date-fns; until then, keep it native.
  • No test framework. Manual verification against live clients.