Client-side SPA — no SSR. All rendering happens in the browser.
Routing
- File-based routing in
routes/.lib/routeTree.gen.tsis auto-generated — never edit it. - Route groups:
(app)/= protected,(auth)/= public. Parentheses don't affect URLs. route.tsxin a group = layout with sharedbeforeLoad; individual files for pages.
Authentication
- Session state via
useSessionQuery()fromlib/queries/session.ts. NEVER useauth.useSession()— TanStack Query provides caching, multi-tab sync, and consistency. - Auth guard in
beforeLoad, not in components. Uses cache-first (getCachedSession()), thenfetchQuery(). - Must validate both
userANDsession(not just one). - After login: call
revalidateSession(queryClient, router)— removes cache + invalidates router sobeforeLoadfetches fresh data, then navigate. - Safe redirects: use
getSafeRedirectUrl()forreturnTosearch params (prevents open redirects). signOut(queryClient)clears server session, invalidates cache, redirects to/login.
tRPC Client
credentials: "include"for cookie-based auth, batched viahttpBatchLink.- 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:
<Link>from TanStack Router withactivePropsfor active styling. Never use<a>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().