Client-side SPA — no SSR. All rendering happens in the browser.
## Routing
- File-based routing in `routes/`. `lib/routeTree.gen.ts` is auto-generated — never edit it.
- Route groups: `(app)/` = protected, `(auth)/` = public. Parentheses don't affect URLs.
- `route.tsx` in a group = layout with shared `beforeLoad`; individual files for pages.
## Authentication
- Session state via `useSessionQuery()` from `lib/queries/session.ts`. NEVER use `auth.useSession()` — TanStack Query provides caching, multi-tab sync, and consistency.
- Auth guard in `beforeLoad`, not in components. Uses cache-first (`getCachedSession()`), then `fetchQuery()`.
- Must validate both `user` AND `session` (not just one).
- After login: call `revalidateSession(queryClient, router)` — removes cache + invalidates router so `beforeLoad` fetches fresh data, then navigate.
- Safe redirects: use `getSafeRedirectUrl()` for `returnTo` search params (prevents open redirects).
- `signOut(queryClient)` clears server session, invalidates cache, redirects to `/login`.
## tRPC Client
- `credentials: "include"` for cookie-based auth, batched via `httpBatchLink`.
- API URL: `${import.meta.env.VITE_API_URL || "/api"}/trpc`.
- Uses `createTRPCOptionsProxy()` for TanStack Query integration.
## Components
- Named exports, functional only. shadcn/ui from `@repo/ui`.
- Navigation: `` from TanStack Router with `activeProps` for active styling. Never use `` for internal routes.
- Route context: `Route.useSearch()` for search params, `Route.useRouteContext()` for route data.
- Jotai store available for cross-route UI state (modals, sidebar).
## Error Handling
- `AppErrorBoundary` (root) shows generic error UI. `AuthErrorBoundary` (protected routes) catches 401/UNAUTHORIZED and shows sign-in recovery UI; 403 falls through to generic handler.
- Utilities in `lib/errors.ts`: `getErrorStatus()`, `isUnauthenticatedError()`, `getErrorMessage()`.