These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

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:

  1. Reads the JWT from authStore (Zustand)
  2. Redirects to /login if no token is present
  3. Calls GET /users/me to sync the current user’s role + subscription tier
  4. Renders the sidebar + job tray + alerts wrapper
  5. 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.tsx lists finders; app/finders/[id]/page.tsx is the editor for one. Same shape used for locations, maps, sets, tags.
  • Singletons: app/dashboard, app/account, app/billing, app/changelog, app/admin are single-page surfaces with no list/detail split.
  • Workspaces: gated additionally by the backend’s subscription_tier:premium middleware. 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:

  1. api/ext/v2/snippet/route.ts — serves the widget snippet JS. In dev, it reads from the sibling-path dropafinder-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.
  2. 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:

  1. Global SCSS is imported (the design system tokens, base reset, theme variables)
  2. The <Providers> component wraps {children} to install QueryClient (TanStack Query) and ThemeProvider (next-themes for data-theme attribute 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.

A few routing-shaped concerns live elsewhere:

  • src/components/layout/ — sidebar, job tray, panel container. The chrome rendered by AppShell.
  • src/components/auth/ — login and register form components, used by the (auth) routes.
  • src/stores/authStore — JWT + user state. Read by AppShell to decide redirect.
  • src/providers/Providers.tsx — QueryClient + ThemeProvider; mounted by layout.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 entire edit/ 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