Routing Architecture
Next.js App Router Structure
The application uses the Next.js 14 App Router with three top-level layouts and several nested route groups.
app/
├── layout.tsx # Root layout (fonts, providers)
├── globals.css
│
├── (base-layout)/ # No URL segment — pages outside any team scope
│ ├── page.tsx # / (root / team chooser)
│ ├── account/page.tsx # /account
│ ├── no-access/page.tsx # /no-access
│ └── ...
│
├── auth/ # Public auth flows
│ ├── sign-in/page.tsx
│ ├── sign-up/page.tsx
│ ├── callback/route.ts
│ ├── confirmations/...
│ └── restricted/page.tsx
│
├── [primaryTeam]/ # Dynamic team segment
│ ├── (team-layout)/ # Team-scoped pages
│ │ ├── layout.tsx # Calls getRoutePermissions({ teamId }) + redirects
│ │ ├── page.tsx # /[primaryTeam] (team dashboard)
│ │ ├── campaigns/... # /[primaryTeam]/campaigns
│ │ ├── members/... # /[primaryTeam]/members
│ │ ├── teams/... # /[primaryTeam]/teams (super-admin)
│ │ ├── roles/... # /[primaryTeam]/roles
│ │ ├── permission-keys/...
│ │ ├── permission-set/...
│ │ ├── credentials/...
│ │ ├── voter-search/page.tsx
│ │ ├── admin-voter-search/page.tsx
│ │ ├── all-turn-ins/... # team-wide accounting fallbacks
│ │ ├── rates/...
│ │ ├── transactions/...
│ │ ├── regions/...
│ │ └── circulator/ # Circulator dashboards
│ │ ├── by-campaign/page.tsx
│ │ └── by-turn-in-date/page.tsx
│ │
│ └── (campaign-layout)/ # Campaign-scoped pages
│ └── campaign/[slug]/ # Nested dynamic segment
│ ├── layout.tsx # Calls getRoutePermissions({ teamId, campaignId })
│ ├── page.tsx # /[primaryTeam]/campaign/[slug]
│ ├── add-signatures/...
│ ├── signatures/...
│ ├── petitions/...
│ ├── petition-format/...
│ ├── dashboard/...
│ ├── admin-dashboard/...
│ ├── validators-view/...
│ ├── circulators-view/...
│ ├── households-view/...
│ ├── sub-teams/...
│ ├── transactions/...
│ ├── turn-in/...
│ ├── rates/...
│ ├── regions/...
│ └── no-access/page.tsx
│
└── api/ # Route handlers
├── essearch/route.ts
├── essearch-common/route.ts
├── duplicate/route.ts
├── charts/[id]/route.ts
└── permissions/revalidate/route.ts
Route patterns
1. Static routes
Auth flows under app/auth/* and the root chooser (base-layout)/page.tsx.
2. Dynamic segments
[primaryTeam]— team slug. Read fromparamsin server components or viauseParams()in client components.[slug]— campaign id (often a slug). Sits under(campaign-layout)/campaign/[slug].[id]— record id used in nested routes likepermission-set/[id]/edit.
3. Route groups
Folders wrapped in parentheses (e.g. (team-layout), (campaign-layout)) group related routes under a shared layout.tsx without appearing in the URL.
4. Layout composition
Every nested layout inherits the parents:
RootLayout
└─ (base-layout) | (team-layout) | (campaign-layout)
The team and campaign layouts are responsible for permission gating before their children render. See Middleware & Route Protection.
Accessing route parameters
Server Components
export default async function Page({
params,
}: {
params: { slug: string; primaryTeam: string };
}) {
const campaign = await getCampaignBySlug(params.slug);
}
Client Components
"use client";
import { useParams } from "next/navigation";
export function MyComponent() {
const params = useParams<{ primaryTeam: string; slug: string }>();
// params.primaryTeam, params.slug
}
Search params
export default async function Page({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined };
}) {
const step = searchParams?.step ?? "1";
}
Search params are also used to encode shareable table state (filters, sorting, pagination) — the table primitives in src/components/tables/common/ read them via useSearchParams() on the client and searchParams on the server.
Special pages
| Path | Purpose |
|---|---|
/no-access | Team-level access denial |
/[primaryTeam]/campaign/no-access | Campaign-level access denial (inside the campaign layout) |
/auth/restricted | Account is blocked |
/auth/sign-in | Sign-in target for unauthenticated requests |
The "no-access" pages render a <RefreshPermissionsButton /> (src/components/common/refresh-permissions-button.tsx) that calls POST /api/permissions/revalidate so a freshly granted permission becomes effective without a full sign-out.