Introduction
Project Overview
Purpose of the Project
- Primary goal: A petition validation and campaign management platform that helps teams organize campaigns, manage teams and roles, collect and validate petition signatures, and search voter records.
- Problems it solves:
- Centralizes petition workflows (campaign setup, formats, circulators, signature entry and validation).
- Enforces fine-grained, layered permissions across teams, sub-teams, and campaigns.
- Integrates voter search via an external service (Directus-backed Elasticsearch) for validation.
- Prevents duplicate signers per campaign using Redis.
- Manages invitations and onboarding via Supabase Auth and Edge Functions.
- Provides configurable per-campaign dashboards and chart generation.
- Tracks turn-ins, rates, transactions, and regions for campaign accounting.
High-level Architecture
-
Front-end: Next.js 14 App Router (React 18, TypeScript, TailwindCSS, Radix UI, shadcn/ui).
- Route structure under
src/appwith three top-level layouts:src/app/(base-layout)— root pages outside any team scope (landing/team chooser, account,no-access).src/app/[primaryTeam]/(team-layout)— team-scoped features (campaigns list, members, roles, permission keys/sets, credentials, voter-search, rates, transactions, turn-ins, regions, sub-teams, circulator dashboards).src/app/[primaryTeam]/(campaign-layout)/campaign/[slug]— campaign-scoped features (signatures entry, petitions, dashboards, validators/circulators/households views, sub-teams, transactions, turn-ins, regions).
- Auth flows in
src/app/auth(sign-in, sign-up, callback, confirmations, restricted, invite/login confirmations). - UI components under
src/componentsgrouped by domain (campaign, petitions, signatures, dashboard, permissions, permission-sets, roles, regions, teams, transactions, turn-in, tables, forms, ui, routes). - Sidebar navigation is composed from per-area "route" components under
src/components/routes/*that readpermissionKeysfromTeamsContext.
- Route structure under
-
Backend (within Next.js):
- Edge middleware:
src/middleware.tsdelegates tosrc/lib/supabase/updateSession.tsto:- Hydrate the Supabase session via cookies (uses
getClaims()to verify the JWT locally — no network round-trip per request). - Redirect unauthenticated requests to
/auth/sign-in, preserving the intended path via theredirect_urlcookie. - On a real browser refresh (F5 / Ctrl+Shift+R), POST to
/api/permissions/revalidateto invalidate the cached permissions snapshot for the current user. Edge middleware no longer fetches permissions itself (permission gating moved to layouts).
- Hydrate the Supabase session via cookies (uses
- Layout-based permission gating:
(team-layout)and(campaign-layout)server layouts callgetRoutePermissions(\{ teamId, campaignId \}), thenredirect("/")orredirect("/$\{teamId\}/campaign/no-access")when access is missing. Page-level guards are added withrequireAccess(\{ teamId, campaignId, key \}). - API routes (
src/app/api/...):essearchandessearch-common— proxy to Directus, which authenticates and calls a custom/essearch/validationendpoint for voter/person search. Tokens are cached in HTTP-only cookies.duplicate— checks if a voter has already signed for a given campaign using Redis (Upstash) set membership.permissions/revalidate— drops the current user's cached permissions snapshot viarevalidateTag(\permissions-${userId}`)`. Called by middleware and the "Refresh permissions" button on the access-denied page.charts/[id]— runs configurable aggregations against Supabase tables (count/sum/avg/min/max with grouping and filters) and persists generated chart data.
- Server Actions / Services:
- Domain "services" under
src/services/**use the Supabase server client to query/update Postgres (campaigns, petitions, teams, permissions, transactions, dashboards, regions, etc.). - Server actions under
src/actions/**for mutations (auth, campaigns, petitions, teams, transactions, responsibilities/permission-sets, dashboards, signatures, credentials, role-based-access). - Access-control helpers live in
src/services/accessCheck/**(accessCheck,getRoutePermissions,requireAccess).
- Domain "services" under
- Edge middleware:
-
Data Stores
- Supabase Postgres: Primary system of record for users, teams, campaigns, petitions, signatures, permission keys/sets, role assignments, dashboards, charts, transactions, turn-ins, rates, regions.
- Redis (Upstash): Per-campaign signer set for fast duplicate detection (
campaign:\{campaignId\}:signers). - Cookies: Supabase SSR session cookies, short-lived Directus access tokens, and a
redirect_urlcookie used by the edge middleware.
-
External Services
- Supabase:
- Auth (email invitations, session cookies via the SSR client, JWT claims verification).
- Postgres database via
@supabase/supabase-jsand@supabase/ssr. - Edge Functions:
get-permissions-snapshot,send-email,generate-chart-data,refresh-all-charts,sync-to-redis.
- Directus:
- Authentication and REST client via
@directus/sdk. - Proxies an Elasticsearch-backed endpoint (
/essearch/validation) for voter/person search.
- Authentication and REST client via
- Upstash Redis:
- TLS-enabled Redis used for signature duplication checks.
- Deployment:
- Vercel (
vercel.jsonpresent). Git-based deployments enabled formain,dev,staging.
- Vercel (
- Supabase:
-
Security and Access Control
- Edge middleware enforces authenticated access outside
/auth/*and keeps Supabase cookies in sync. - Permission resolution is delegated to the
get-permissions-snapshotedge function, which inlines logic that previously lived in theget_user_combined_permissionsSQL helper. It returns{ teamAccess, campaignAccess?, permissionKeys? }. - Snapshots are memoized per
(userId, teamId, campaignId)usingunstable_cacheand taggedpermissions-$\{userId\}for surgical invalidation. - Layouts and pages call
getRoutePermissions/requireAccessto enforce per-route gates; client UI also receivespermissionKeysviaTeamsContextto conditionally render sidebar items. - Service Role usage is restricted to server-only contexts (e.g., the chart generation route handler and the edge function).
- Role hierarchy (
roles.hierarchy_level) prevents lower-level admins from acting on roles at or above their own level.
- Edge middleware enforces authenticated access outside
Tech Stack
-
Framework
- Next.js 14.2.25 (App Router)
- React 18
- TypeScript 5
- TailwindCSS 3.4,
tailwind-merge,tailwindcss-animate - Radix UI primitives + shadcn/ui (
components.json) @tanstack/react-tablefor complex table UIsreact-hook-form+zod+@hookform/resolversfor formsrechartsfor chartslucide-reactfor icons
-
Backend/runtime
- Next.js API routes and Edge middleware for server logic
- Supabase:
@supabase/ssrfor server/client instantiation and cookie management@supabase/supabase-jsfor DB / auth / edge function invocation
- Supabase Edge Functions (Deno) for permission snapshots, email sending, chart generation, Redis sync
- Directus SDK (
@directus/sdk) for REST and token-based auth to proxy Elasticsearch queries - Redis (
redisnpm package) for duplication checks (Upstash, TLS) - Deployed on Vercel
-
Node / Runtime
- No explicit
enginesfield. Recommended Node 18+ (Next.js 14 compatible). Type definitions target@types/node^20. - Vercel default Node runtime is used.
- No explicit
-
Configuration Highlights
- TypeScript path alias
@/* -> ./src/*. - Next image config allows the Supabase storage host.
- Middleware matcher excludes static assets and images.
- Environment variables expected (see
docs/deployment/environment-variables.md):- Supabase:
NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY, andSERVICE_ROLE_KEY(server only). - Super-admin:
SUPER_ADMIN_TEAM_ID(server only; surfaced to clients viaTeamsContext). - Directus: dynamic credentials provided per request (
credentials.\{name,emailKey,passwordKey,endpointKey\}) or fixed envs for common search:DIRECTUS_API_EMAIL_SEARCH,DIRECTUS_API_PASSWORD_SEARCH,DIRECTUS_URL_SEARCH
- Redis:
REDIS_URL
- Supabase:
- TypeScript path alias
-
Notable App Conventions
src/services/**: server-side data access using the Supabase server client. ReturnsQueryResponse({ message, error, data }).src/actions/**: server actions that handle create/update flows for key entities. ReturnsFormState({ message, error, data?, zodIssues? }).src/middleware/**: legacy permission pipelines and validators retained for reference. The active middleware only manages the Supabase session — page/layout-level gating is the source of truth.src/services/accessCheck/**: route-level permission checks (accessCheck,getRoutePermissions,requireAccess).src/lib/permissions/**: edge-safe helper for fetching the permissions snapshot.src/lib/team-member-hierarchy.ts+src/services/roles/getUserRoleHierarchyLevelForTeam.ts: role-hierarchy gates used by member-management actions.src/components/tables/**: rich data tables (sorting, filtering, pagination) powered by TanStack Table and theuseFetchTableDatahook.src/components/routes/**: per-section sidebar navigation (campaign, campaign admin, accounting, common, super-admin, team-management, circulator, user-settings) that readspermissionKeysfromTeamsContext.src/lib/supabase/**: SSR-safe client setup for browser, server, and a special server client for service-role operations.supabase/functions/**: Deno Edge Functions for permission snapshots, email, chart generation/refresh, and Redis sync.
Where to go next
| Topic | Doc |
|---|---|
Folder-by-folder breakdown of src/ | Folder / File Structure |
| App Router routes, layouts and dynamic segments | App Directory |
| Server-side guards (middleware + layouts + pages) | Middleware & Route Protection |
| Permission resolution + caching + invalidation | Permissions Snapshot |
| Role hierarchy and member-management gates | Role Hierarchy |
API route handlers under app/api/** | API Route Handlers |
| Sidebar route components pattern | Sidebar Route Bundles |
| Supabase Edge Functions and migrations | Supabase Backend |
| Deployment + environment variables | Environment Variables |