Skip to main content

Middleware Directory

Location: src/middleware/

Status — read this first.

Today the live Next.js Edge middleware (src/middleware.ts) does only session sync (see src/lib/supabase/updateSession.ts). The route-pipeline files described below are dormant reference code that pre-date the move to layout-based permission gating. They are intentionally kept around because:

  • The SessionContext type defined in middleware/types.ts is still consumed by TeamsContext (src/context/TeamsContext.tsx).
  • decodeJWT.ts is still occasionally useful when debugging.

If you are looking for "how does access control actually work today?" jump to:

Structure

middleware/
├── routes/ # Per-section route handler bundles (legacy)
│ ├── campaigns.ts
│ ├── credentials.ts
│ ├── permission-keys.ts
│ ├── roles.ts
│ └── teams.ts

├── validators/ # Permission validators used by the legacy pipelines
│ ├── api-routes.ts # voter-search / essearch handlers
│ ├── campaign-route.ts # checkCampaignHome / checkCampaignGenFeat
│ └── team-route.ts # checkPrimaryTeamAllowed / checkGenFeat

├── routePipelines.ts # Pattern → handlers map (not invoked by `middleware.ts`)
├── types.ts # SessionContext + handler context types (still used by TeamsContext)
└── utils.ts # Helpers for the legacy pipeline (URL parsing etc.)

What the live middleware does

src/middleware.ts simply delegates to updateSession:

export async function middleware(request: NextRequest) {
return await updateSession(request);
}

export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};

updateSession (in src/lib/supabase/updateSession.ts):

  1. Refreshes Supabase cookies and verifies the JWT locally with getClaims() (no network call per request).
  2. Honors the redirect_url cookie set during sign-in.
  3. Redirects unauthenticated requests to /auth/sign-in, persisting the intended path via the redirect_url cookie.
  4. On a real browser refresh (F5 / Ctrl+Shift+R, detected via sec-fetch-dest=document + cache-control: no-cache), POSTs to /api/permissions/revalidate so the next render reads a fresh permissions snapshot.

Where permission gating actually lives now

SurfaceMechanism
Authenticated session enforcementmiddleware.tsupdateSession
Team scope (e.g. /[primaryTeam])(team-layout)/layout.tsx calls getRoutePermissions({ teamId }) and redirect()s on no access
Campaign scope(campaign-layout)/campaign/[slug]/layout.tsx calls getRoutePermissions({ teamId, campaignId })
Page-level permission keysrequireAccess({ teamId, campaignId, key }) in src/services/accessCheck/requireAccess.ts
Sidebar / conditional UIClient components read permissionKeys from TeamsContext