Surplus logo
Surplus Docsby Sharing Excess
Architecture

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.md describes 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 in docs/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 SelectCombobox for static option lists and SearchSelect for 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