Skip to main content

Data Fetching & Auth

How the web app talks to the backend, and how sessions work. Understanding this page saves the most onboarding time — it is the least obvious part of the codebase.

The proxy layer

There is no Vite dev proxy. Instead, the app ships its own same-origin proxy built from TanStack Start server routes under src/routes/api/**:

Browser ──▶ /api/os-tickets/… (same origin, TanStack Start server route)

▼ forwardBackendRequest()
VITE_API_BASE_URL + /api/v1/tickets/… (real backend via gateway)
  • Each backend domain has a -proxy.ts helper (e.g. src/routes/api/os-tickets/-proxy.ts, src/routes/api/os-auth/-proxy.ts).
  • All of them delegate to forwardBackendRequest in src/routes/api/-backend-proxy.ts, which rewrites the URL onto VITE_API_BASE_URL plus the versioned backend base path (tickets → /api/v1/tickets, auth → /api/v1/auth/), preserving the query string.
  • src/routes/api/-forward-headers.ts attaches auth: it forwards an incoming Authorization header, otherwise injects Bearer <accessToken> from the stored session. Auth endpoints opt out with includeAuth: false.

Making requests from components

Use requestJson from src/libs/http-client/request.ts:

  • target: "app" — call the same-origin /api/... proxy routes (the normal path; works in SSR too).
  • target: "backend" — hit the backend directly via the axios client (src/libs/http-client/client.ts).

requestJson understands the backend's response envelope { data, message, path, status, timestamp } and unwraps data automatically. Pass a Zod schema to validate responses. A 401 is surfaced as a "session expired" error.

Wrap calls in TanStack Query queryOptions inside the feature's -api/ or -resources/ folder — see any existing feature (e.g. src/routes/dashboard/ticketing/tickets/-api/) for the pattern to copy.

Auth and sessions

Auth is a custom JWT flow implemented in src/libs/auth/:

  • Login methods: credentials (/api/os-auth/admin-login) and Google OAuth (google-start / google-complete, callback route at login/google/callback.tsx).
  • Session storage: a cookie (name from AUTH_SESSION_COOKIE_NAME, samesite=lax) holding URL-encoded JSON with access/refresh tokens, role, and permissions decoded from the JWT. Legacy localStorage sessions are auto-migrated to the cookie.
  • SSR: the cookie is readable server-side, so resolveAuthUser() renders the same authenticated UI on the server — no hydration mismatch.
  • Refresh & logout: refreshStoredAuthSession() refreshes an expired access token; logoutCurrentSession() calls /logout and clears the cookie.
  • Route protection: src/routes/dashboard/route.tsx redirects to /login in beforeLoad when there is no auth user, and wraps the dashboard in AccessControlProvider (permission checks come from r3-utils/access-control).
note

better-auth appears in package.json and the README, but the live flow is the custom JWT/cookie implementation described above. Don't assume Better Auth patterns apply.

Environment variables

Validated with @t3-oss/env-core in src/env.ts — the app fails at startup if required values are missing.

VariableScopeNotes
VITE_API_BASE_URLclientRequired URL of the backend gateway (e.g. http://localhost:8080 for local backend)
VITE_LOG_CLIENTclientClient-side logging, default true
BETTER_AUTH_SECRETserverRequired — generate with npx @better-auth/cli secret
SESSION_TIMEOUTserverDefault 300000 ms
LOG_API_CALLSserverLog proxied API calls, default true
LOG_DBserverDefault true

Copy .env.example to .env and fill these in before first run.