Technical docs

Auth and Session

Public reference generated from tech docs/auth-and-session.md.

Overview

Support reference for login, session bootstrap, logout, invite interaction, and cross-tab session sync.

Main frontend routes

- `/login` - `/login/google` - `/invite/[code]`

Main backend routes

- `GET /api/auth/google` - `GET /api/auth/google/callback` - `GET /api/auth/google/profile` - `GET /api/auth/status` - `POST /api/auth/logout` - `GET /api/init/whoami` - Public invite routes: - `GET /api/invites/{code}` - `GET /api/invites/{code}/accept`

Session model

- Sessions use cookie-based auth backed by `express-session` with Redis session storage. - The session cookie name defaults to `sessionId` and can be overridden by `SESSION_COOKIE_NAME`. - The backend session stores: - `userId` - `wsToken` - `authMiddleware` requires `req.session.userId` and resolves `req.user`. - `optionalAuthMiddleware` resolves `req.user` when a valid session exists, but does not require one.

`/api/init/whoami`

- This is the frontend's real bootstrap endpoint. - If there is no valid session or no `wsToken`, it returns: - `user: null` - `wsToken: null` - If the session user no longer exists, it also returns `user: null` and `wsToken: null`. - If the user exists but has no `currentOrganization`, the backend auto-selects one: - first existing organization for that user - or a newly created default organization named `My Organization` - On success it returns: - `user: user.safeData()` - `wsToken` - The frontend uses this response to populate: - current user - current organization - websocket token

Frontend auth lifecycle

- `AuthContext` calls `/api/init/whoami` on mount. - It applies: - 10 second request timeout - one automatic retry after 2 seconds for network / timeout failures only - Backend `is_admin` is mapped to frontend `is_developer`. - Backend timestamps are mapped into frontend ISO date fields. - Logout posts to `/api/auth/logout`. - On successful logout, the frontend clears auth state and local websocket token. - If `/api/init/whoami` still fails after the retry, the frontend keeps the user unauthenticated and sets `authError` to `network` or `timeout`.

Google login flow

- `/login` is an auto-redirect page, not a form. Once auth bootstrap finishes and the user is not authenticated, it requests `GET /api/auth/google` and redirects the browser to the returned `auth_url`. - `/login` forwards an optional `state` query parameter to `GET /api/auth/google`. - `/login/google` reads the Google `code` and optional `state`, calls `GET /api/auth/google/callback`, then: - shows the backend `message` when present - calls `refetchUser()` - routes to `redirect_url` - falls back to `/login` when no redirect is returned - In the callback, the backend: - exchanges the Google code - creates or updates `User` and `UserGauth` - sets `req.session.userId` - generates and stores `req.session.wsToken`

Invite interaction

- Invite-aware login passes OAuth `state` as JSON: `{"inviteCode":"<code>"}`. - In the Google callback: - the backend parses `state` - saves the session - then attempts invite acceptance when `inviteCode` is present - Successful invite-based sign-in returns `redirect_url: '/'`. - If callback-time invite acceptance fails with an app error, the backend returns `success: false` and `redirect_url: /invite/{code}` so the frontend lands back on the invite page. - Invite-based sign-up skips the normal non-invite onboarding redirect. - For invite-page behavior itself, see invite-flow.md.

New-user bootstrap

- New Google users without an invite get a default organization. - The callback then returns the onboarding redirect from `getOnboardingRedirectUrl(organization?.id)`. - Current onboarding helper behavior is: - `/staff/ai/{employee_id}?onboarding=true` when the organization already has an AI teammate - `/` when it does not - Existing users without invite flow are redirected to `/`.

Cross-tab session sync

- The frontend syncs session state across tabs in three ways: - `BroadcastChannel` messages for org changes and logout - Zustand subscription broadcasting current user / org changes - fallback `/api/init/whoami` check after a tab becomes visible after 30s hidden - Mismatch states shown by the UI are: - `user_changed` - `org_changed` - `logged_out` - The recovery path is a hard reload.

Support checks

- If login appears stuck: - inspect `GET /api/auth/google` for a returned `auth_url` - inspect `/api/init/whoami` - inspect `/api/auth/google/callback` - check the session cookie name and Redis-backed session storage - If the user lands in the wrong org: - inspect `currentOrganization` in `/api/init/whoami` - inspect whether the frontend received an org-change sync event from another tab - If invite login behaves differently from self-serve login: - confirm whether OAuth `state` included JSON with `inviteCode` - confirm whether invite acceptance succeeded in the callback path

Start building your AI team