Tech Stack
Every dependency we ship and why it is in the bundle.
Runtime & framework
| Package | Version | Why |
|---|---|---|
| next | 16.x | App Router, Server Components, route handlers for /api. |
| react / react-dom | 19.x | Pinned by Next 16. |
| typescript | 5.x | Strict mode is enabled. |
| @supabase/ssr | latest | Cookie-aware Supabase client for SSR + middleware. |
| @supabase/supabase-js | latest | Browser-side queries. |
Data, charts, PDF
| Package | Used for |
|---|---|
| @tanstack/react-query | Cache for some integration fetches. Most metric fetches use plain useEffect + fetch — react-query is opt-in. |
| chart.js + react-chartjs-2 | All charts on the dashboard. Components live in src/components/dashboard/. |
| @react-pdf/renderer | Server-rendered PDFs from React components in src/components/reports/. |
| lucide-react | Every icon in the app. Do not import from any other icon set. |
| googleapis | Official 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.
| Token | Value | Use |
|---|---|---|
| --font-display | 'Montserrat', system-ui, sans-serif | h1/h2/h3 + the .font-display utility class. |
| --font-sans | 'DM Sans', 'Inter', system-ui, sans-serif | Body, paragraphs, MetricCard rows, all default text. |
| --black | #0e0e0e | Tinted near-black. Default text. HubNav background. |
| --orange | #F26419 | FPM brand orange. CTAs, current phase, accent rules. |
| --orange-mid | #f7812e | Hover state for orange surfaces. Tier badge text fallback. |
| --off-white | #F8F7F5 | Page background — tinted warm, not pure white. |
| Sidebar bg | #202020 | Slightly 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.
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:
useStatefor UI state (active tab, date range, modal open).useEffect+fetchfor metric data, keyed onclient.idand the date range.@tanstack/react-queryonly where dedup or background refetch is genuinely needed.
Path alias
@/* maps to ./src/*. Always import via the alias, not relative paths:
// 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
DateandtoISOString().slice(0, 10). If date math gets gnarly we will adddate-fns; until then, keep it native. - No test framework. Manual verification against live clients.