Frontend
The frontend is a Next.js 16 App Router application. Most pages are Server Components that delegate to 'use client' feature components. Dark-only UI via CSS variables.
Page routes
All /{org}/** pages are gated by ProtectedRoute. Unauthenticated → hard redirect to /api/auth/start.
| Route | What’s there |
|---|---|
/ | Marketing homepage |
/account | User account (session info, installations, sign out) |
/{org} | Redirects → /{org}/organization |
/{org}/organization | Organization overview (FeaturePlaceholder stub) |
/{org}/organization/team-management | Bulk-add members to a team |
/{org}/organization/role-management | Bulk member/admin role change |
/{org}/organization/mass-invite | Invite flow: search, JSON, CSV/XLSX |
/{org}/leaderboards | Contributor leaderboard |
/{org}/leaderboards/r | Repository leaderboard |
/{org}/leaderboards/r/[repository] | Single-repo leaderboard |
/{org}/leaderboards/team | Team leaderboard |
/{org}/leaderboards/scoring | Preset CRUD editor |
/{org}/analytics | Org dashboard (stats + trend + top contributors) |
/{org}/analytics/contributor | Per-contributor profile + timeline |
/{org}/analytics/repository | Per-repo analytics |
/{org}/analytics/heatmap | 365-day contribution heatmap |
/{org}/analytics/timeline | Activity feed |
Auth gating
OrganizationLayoutClient wraps every /{org}/ page in ProtectedRoute:
- Reads
useAuthStore— hydrated byuseAuthSession(TanStack →GET /api/auth/session) - Loading: shows spinner
- Unauthenticated:
window.location.replace('/api/auth/start?returnTo=<path>') - Authenticated: renders children
InstallationAccessBanner renders above content but below the gate — it’s a non-blocking warning for missing installations or permissions. It does not block page rendering.
Data fetching
All data flows through TanStack Query (hooks/queries/, hooks/mutations/). Components never call fetch() directly. QueryProvider wraps the app with staleTime: 30s, refetchOnWindowFocus: false.
Query keys are centralized in hooks/query-keys.ts. Five hooks bypass it with inline keys — a known inconsistency.
Key override: leaderboard queries use staleTime: 24h (data is precomputed and served from cache). Auth session queries use staleTime: 0 (always refetch on mount).
Mutation hooks (8 total) invalidate relevant query caches on success via the QueryClient.
State stores
Five Zustand stores, all ephemeral in-memory. No localStorage.
| Store | Holds | Populated by |
|---|---|---|
auth-store | Session object, loading, errors | useAuthSession hook |
workspace-store | Active org slug, analytics date range | Org switcher in sidebar |
leaderboard-store | Sort/filter preferences | Leaderboard pages |
url-params-store | Last-used params per page (timeRange, presetId) | Navigation helpers, restored on org switch |
analytics-store | Selected contributor, repo, date range | Analytics pages |
Charts
- Recharts (
BarChart,LineChart) for leaderboard and analytics views - MiniSparkline — pure SVG polyline, used in contributor and team cards
- ContributionHeatmap — pure CSS 53×7 grid, 5-level emerald color scale
- ActivityTimeline — bespoke zigzag CSS layout
- BrushNavigator — custom drag-handle time selector with raw mouse events
Notable features
- XLSX/CSV import in MassInvite via the
xlsxlibrary — auto-detects columns - cmdk command palette used for contributor search combobox
- framer-motion
AnimatePresencefor card layout animations - RecomputeButton — dev-only FAB (returns
nullin production), calls the recompute endpoint - next-themes is installed but
ThemeProvideris never mounted — the app is hard dark-mode