Frontend architecture
The web client (apps/client) is a Vite single-page app built with React 19 and the TanStack family of libraries. Its job is to render data and dispatch typed API calls — all business logic lives on the server. This page explains how the client is organized so you can find your way around quickly.
Routing
Routing uses TanStack Router with file-based routes under apps/client/src/routes/. The router generates a typed route tree (routeTree.gen.ts) so navigation and route params are fully type-checked.
The most important file is the layout that guards everything behind sign-in:
routes/_authenticated.tsx— wraps all protected routes. It redirects unauthenticated users to sign in, sends suspended accounts to a dedicated screen, enforces the terms-acceptance gate, and applies per-role allowlists so partners and drivers only reach the routes meant for them.
Route-local helper code (components, hooks, and utilities used by a single route) lives in a sibling -helpers/ folder. The leading dash tells the router to ignore it for routing — it's just colocated code. This is a convention you'll see throughout the routes tree.
Data fetching
Data flows through TanStack Query, wired to the API by the ORPC client in apps/client/src/helpers/orpc.ts. That file exposes an orpc object with typed query and mutation options for every endpoint in the contract.
Reads use useSuspenseQuery inside a Suspense boundary:
const { data } = useSuspenseQuery(orpc.hubs.list.queryOptions({ input }))Writes use useMutation:
const mutation = useMutation({
...orpc.routes.create.mutationOptions(),
onError: toastOrpcError,
})Because the client is generated from the shared contract, the types for input and data are exact — if the contract changes, the client stops type-checking until you update the call.
Cache invalidation
After a mutation, you invalidate the affected queries so the UI refetches. ORPC query keys are structured ([['hubs','getInventory'], { input, type }]), so orpc.ts ships a handful of targeted invalidation helpers — for example invalidateHubInventoryQueries(queryClient, hubId) — that match the right keys regardless of sort or pagination.
Errors and request IDs
Every failed call carries a requestId from the server. The client surfaces a short prefix in error toasts and offers a "Copy ID" action, so a user reporting a problem can hand you an id that maps directly to server logs and traces. Use toastOrpcError as a mutation's onError to get this behavior for free.
State and forms
- Server state lives in TanStack Query — it's the cache of everything the API returns. You rarely store fetched data anywhere else.
- Form and local UI state uses TanStack Store. Forms are built on it rather than a separate form library.
- URL-backed overlays — many panels, drawers, and detail views are driven by URL search params rather than local component state, so they're linkable, shareable, and survive refreshes. The internal
docs/overlays.mddescribes the pattern and which overlays are URL-backed versus local-only.
UI and styling
- Components are built on shadcn-style primitives over Base UI, styled with Tailwind CSS v4.
- Semantic tokens only — use
bg-background,text-foreground, and friends, never raw colors or legacy token names. The design system is documented indocs/design.md. - Every interactive element gets a stable ID following the
{routeName}{sectionName}{elementName}convention. This powers reliable end-to-end test selectors. See Conventions. - Pickers — use
SelectComboboxfor static option lists andSearchSelectfor database-backed lists; never wire up raw select primitives in feature code.
Maps
Route planning and the impact map use Mapbox. Map styling follows the same semantic-token approach so markers and overlays stay consistent across light and dark themes.
Where to go next
- The web client — package details and scripts.
- API and contracts — the typed client you're calling.
- Conventions — naming, IDs, tokens, and file organization.