Middleware Directory
Location: src/middleware/
Status — read this first.
Today the live Next.js Edge middleware (
src/middleware.ts) does only session sync (seesrc/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
SessionContexttype defined inmiddleware/types.tsis still consumed byTeamsContext(src/context/TeamsContext.tsx).decodeJWT.tsis 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):
- Refreshes Supabase cookies and verifies the JWT locally with
getClaims()(no network call per request). - Honors the
redirect_urlcookie set during sign-in. - Redirects unauthenticated requests to
/auth/sign-in, persisting the intended path via theredirect_urlcookie. - On a real browser refresh (F5 / Ctrl+Shift+R, detected via
sec-fetch-dest=document+cache-control: no-cache), POSTs to/api/permissions/revalidateso the next render reads a fresh permissions snapshot.
Where permission gating actually lives now
| Surface | Mechanism |
|---|---|
| Authenticated session enforcement | middleware.ts → updateSession |
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 keys | requireAccess({ teamId, campaignId, key }) in src/services/accessCheck/requireAccess.ts |
| Sidebar / conditional UI | Client components read permissionKeys from TeamsContext |