Next.js — routing
The dashboard uses the App Router (src/app/) introduced in Next.js 13 and refined in Next.js 16. There are five top-level groupings; this page maps each one and points at the file responsible for it.
If you’re new to the codebase, read this in tandem with Architecture overview. The router is where every dashboard interaction begins; understanding the route map answers most “where do I add X?” questions immediately.
Top-level structure
src/app/
├── layout.tsx # Root layout: SCSS + Providers
├── (auth)/ # Group: unauthed flows, no chrome
│ ├── login/
│ ├── register/
│ └── register/[token]/ # Invite-token registration
├── (marketing)/ # Group: public landing
│ └── page.tsx
├── (preview)/ # Group: PoC + preview routes (out of customer scope)
│ ├── finder-builder-v3-poc/
│ └── finder-preview/
├── app/ # Protected /app/* dashboard
│ ├── AppShell.tsx # Auth gate + sidebar wrapper
│ ├── dashboard/
│ ├── finders/
│ │ ├── page.tsx # Finders list
│ │ └── [id]/page.tsx # Finder Builder
│ ├── locations/
│ ├── maps/
│ ├── sets/
│ ├── tags/
│ ├── custom-fields/
│ ├── account/
│ ├── billing/
│ ├── changelog/
│ ├── admin/ # Admin console (role-gated)
│ └── workspaces/ # Premium-tier-gated
└── api/ # Route handlers (server-only)
├── ext/v2/snippet/
│ └── route.ts # Serves the embed widget JS
└── map-style-preview/The App Router uses route groups (folders in parentheses like (auth)) to group routes without affecting the URL path. (auth)/login/page.tsx resolves to /login, not /auth/login. We use route groups to apply different layouts to different sections without nesting URL segments.
Route groups in detail
(auth) — public auth flows
URLs: /login, /register, /register/[token].
These pages render without the dashboard chrome (no sidebar, no auth check). They’re where unauthenticated users land. The invite-token variant (/register/[token]) honors a token from an email invite; the token determines which Workspace the new user joins.
🔒 Internal only: The token verification flow on
GET /api/register/{token}(backend) returns the invitee context that the registration form pre-fills. See reference/api/auth for the API side.
(marketing) — landing
URL: /.
The single public page that’s not under /app/*. Mostly content; the closest thing the codebase has to a “marketing site” today.
(preview) — PoC and preview routes
URLs: /finder-builder-v3-poc, /finder-preview.
Out of scope for customer-facing documentation. These are internal-only routes for testing the v3 PoC and other preview surfaces. New code generally shouldn’t go here; v3 is being evaluated for replacement of v2 (see decisions/two-finder-builder-versions).
app/ — the protected dashboard
URLs: everything under /app/*.
This is the bulk of the codebase. Every route under app/ is gated by the AppShell component, which:
- Reads the JWT from
authStore(Zustand) - Redirects to
/loginif no token is present - Calls
GET /users/meto sync the current user’s role + subscription tier - Renders the sidebar + job tray + alerts wrapper
- Renders the route’s content inside that chrome
If you add a new top-level dashboard route, the convention is to nest it under app/ so it inherits the shell. There is no “do I want the shell or not” choice — every authenticated dashboard page wants it.
Sub-route conventions
- Resource list + detail pattern:
app/finders/page.tsxlists finders;app/finders/[id]/page.tsxis the editor for one. Same shape used for locations, maps, sets, tags. - Singletons:
app/dashboard,app/account,app/billing,app/changelog,app/adminare single-page surfaces with no list/detail split. - Workspaces: gated additionally by the backend’s
subscription_tier:premiummiddleware. The frontend can render the route, but API calls return 403 for non-premium users.
Where the heavy lifting happens
app/finders/[id]/page.tsx is the entry point for the Finder Builder — the largest single feature in the codebase. It mounts FinderBuilderV2 from src/components/finders/v2/FinderBuilderV2.tsx, which orchestrates:
- Server-side data fetch via TanStack Query
- Local edit state via a history reducer (for undo/redo)
- The sidebar nav, the section components, and the Live Preview rail
See finder-builder-v2 for the deeper component map.
api/ — route handlers
URLs: everything under /api/* on the dashboard’s origin.
Most of the dashboard’s data comes from the backend API (api.locationfinders.com/api/), not from these route handlers. The handlers under src/app/api/ exist for two reasons:
api/ext/v2/snippet/route.ts— serves the widget snippet JS. In dev, it reads from the sibling-pathdropafinder-app-external/ext/2/snippet.{dev.,}js. In prod, it falls through to the Cloudflare CDN. This route exists so local development of the widget doesn’t require a CDN deploy.api/map-style-preview/route.ts— generates a map-style preview image. Used by the dashboard’s Map style picker.
🔒 Internal only: Don’t add new business logic to
api/route handlers. Business logic belongs in the backend. The route handlers exist for serving artifacts (the snippet) and for purely-frontend concerns (map style preview); both have legitimate “this would be awkward to do from the backend” reasons.
Root layout
src/app/layout.tsx wraps every route — including the route groups above. Two things happen here:
- Global SCSS is imported (the design system tokens, base reset, theme variables)
- The
<Providers>component wraps{children}to install QueryClient (TanStack Query) and ThemeProvider (next-themes fordata-themeattribute switching)
If you’re touching layout.tsx, you’re affecting every page. Almost always the right move is to add to <Providers> instead, or to push concern into a more specific layout file.
How protection works
Auth gating is client-side, not middleware-based. The AppShell component does the JWT check inside React, not in a Next.js middleware function. This is intentional:
- The dashboard is a Vercel-hosted static-ish app; middleware would force every request through the edge runtime
- The protection model is “redirect to login, fetch user, render or bounce” which fits cleanly inside a top-level component
- The actual security boundary is the backend — every API call requires the JWT, and the backend rejects unauthenticated requests regardless of what the dashboard renders
This means: rendering /app/finders without a token momentarily shows a loading state before redirecting. That’s expected; no data leaks because no API call succeeds without the token.
🔒 Internal only: If you add Next.js middleware for some reason (geo-redirects, etc.), make sure it doesn’t accidentally skip the AppShell auth flow. The current setup has zero middleware files; keep it that way unless you have a specific need.
Adding a new route
Three common cases:
Case 1 — a new dashboard page
You want /app/something to exist. Add src/app/app/something/page.tsx. It inherits AppShell automatically; no other wiring needed.
If the page needs a sidebar entry, add it to the sidebar component (typically in src/components/layout/).
Case 2 — a new public page
You want /something to render without auth. Add src/app/(marketing)/something/page.tsx (if it’s marketing-shaped) or under one of the existing route groups that fits.
Case 3 — a new API route handler
You want /api/something/route.ts. Add src/app/api/something/route.ts with a GET/POST/etc. handler exported. Re-read the “don’t add business logic” guidance above before doing this — usually the right home is the backend.
For a deeper recipe that crosses the backend and the dashboard, see how-to/add-api-endpoint-end-to-end.
Routing-related files outside src/app/
A few routing-shaped concerns live elsewhere:
src/components/layout/— sidebar, job tray, panel container. The chrome rendered byAppShell.src/components/auth/— login and register form components, used by the(auth)routes.src/stores/authStore— JWT + user state. Read byAppShellto decide redirect.src/providers/Providers.tsx— QueryClient + ThemeProvider; mounted bylayout.tsx.
Open questions
🔴 [NEEDS CLARIFICATION: Are any production routes still using the legacy
src/components/finders/edit/editor? If yes, document the route(s); if no, recommend deletion of the entireedit/directory. This affects the v2 vs. legacy decision page too.]
🔴 [NEEDS CLARIFICATION: Confirm the
(preview)group is internal-only and never shipped to customers. The route names (finder-builder-v3-poc) suggest yes, but worth confirming.]
Where to next
- Component-level deep-dive on the Finder Builder: finder-builder-v2
- API call wiring (every dashboard route ultimately makes API calls): api-layer
- State management (auth, workspace, UI, processes): state-management
- Cross-codebase view of the routes: Architecture overview