Technical docs
Auth and Session
Overview
Support reference for login, session bootstrap, logout, invite interaction, and cross-tab session sync.
Main frontend routes
- `/login`
- `/login/google`
- `/login/telegram`
- `/desktop-auth/authorize`
- `/settings`
- `/settings/connectedAccounts`
- `/invite/[code]`
Main backend routes
- `GET /api/auth/google`
- `GET /api/auth/google/callback`
- `GET /api/auth/google/profile`
- `GET /api/auth/telegram`
- `GET /api/auth/telegram/callback`
- `DELETE /api/auth/telegram`
- `GET /api/auth/status`
- `POST /api/auth/logout`
- `GET /api/init/whoami`
- Local dev auth routes:
- `GET /api/auth/dev/users`
- `POST /api/auth/dev/login`
- 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 still the main sign-in entry, but it no longer always redirects straight to Google.
- Once auth bootstrap finishes and the user is not authenticated, `/login` first calls `GET /api/auth/dev/users`.
- If that request returns one or more users, `/login` renders a `Development sign-in` picker with:
- one button per returned user
- an `Admin` badge for `is_admin` users
- a fallback `Sign in with Google` action
- If the dev-users request fails or returns no users, `/login` falls back to `GET /api/auth/google` and redirects to the returned `auth_url`.
- `/login` forwards an optional `state` query parameter to Google sign-in and also forwards `select_account=true` when present.
- `/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
- Desktop-auth callback redirects are special-cased:
- if `redirect_url` starts with `/api/desktop-auth/`, the frontend does a full browser redirect to `NEXT_PUBLIC_API_URL + redirect_url`
- if `redirect_url` starts with `/desktop-auth/`, the frontend rewrites it to `NEXT_PUBLIC_API_URL + /api + redirect_url` and then does a full browser redirect
- other app-local redirects still use client-side routing
- 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`
Local development auth flow
- Local dev auth is not available in production.
- The backend returns `404 Not found` for these routes when any of the following is true:
- `NODE_ENV === 'production'`
- `LOCAL_DEV_AUTH_ENABLED === 'false'`
- the app is not in `development` and `LOCAL_DEV_AUTH_ENABLED !== 'true'`
- the request remote address is not loopback
- the request host is not loopback
- the request `Origin` header is present but is not loopback
- `GET /api/auth/dev/users` returns sanitized active-user choices for the picker.
- `POST /api/auth/dev/login` accepts:
- `user_id`
- optional `state`
- A successful dev login creates the same browser session fields as Google OAuth:
- `req.session.userId`
- `req.session.wsToken`
- If the provided `state` JSON includes `inviteCode`, the backend also attempts invite acceptance before returning.
Desktop auth authorization flow
- `/desktop-auth/authorize` is the browser confirmation screen for desktop sync authorization.
- The page reads these query params:
- `return_to`
- `consent_token`
- `device_name` with fallback `Desktop sync app`
- If the user is not authenticated, the page shows a sign-in prompt and links back into `/login` with a desktop-auth state payload.
- That desktop-auth state currently stores `desktopAuthReturnTo` inside the login `state` JSON.
- If the user is already authenticated, the page shows:
- the signed-in Alloy account name and email
- `Continue as ...`
- `Use another account`
- `Cancel`
- `Use another account` logs the current user out and restarts `/login` with both the desktop-auth state and `select_account=true`.
- Desktop authorization only proceeds when `return_to` resolves to `/desktop-auth/authorize` or `/api/desktop-auth/authorize` and does not already contain sensitive auth params such as access tokens, refresh tokens, codes, or ID tokens.
- If validation fails, the page shows `This authorization request is invalid.` and disables continuation.
Telegram account linking flow
- `/settings` currently redirects to `/settings/connectedAccounts`.
- The connected-accounts page lists:
- `Gmail`
- `Telegram`
- Gmail is display-only on that page and reflects the current Google-linked profile.
- Telegram row behavior:
- unlinked state shows `Connect`
- linked state shows `Connected` plus a `Disconnect` action behind a confirmation dialog
- the displayed Telegram identifier prefers `@username`, then linked name, then `telegram_id`
- Connecting Telegram starts with `GET /api/auth/telegram` and redirects the browser to the returned auth URL.
- `/login/telegram` is the frontend callback landing page. It reads the `code` query param, calls `GET /api/auth/telegram/callback`, updates the current user when a user payload is returned, and then redirects to the backend-provided `redirect_url` after a short success delay.
- The Telegram auth routes are session-protected in the backend, so the current implementation links Telegram to the already authenticated user rather than acting as a standalone anonymous sign-in bootstrap.
- `DELETE /api/auth/telegram` unlinks Telegram from the current authenticated user and returns the updated user payload.
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.
- The local dev auth login route uses the same `inviteCode` parsing pattern when `state` is provided.
- 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 local dev sign-in appears on a non-local environment or does not appear locally:
- inspect `NODE_ENV`
- inspect `LOCAL_DEV_AUTH_ENABLED`
- inspect request host, origin, and remote address loopback checks
- inspect `GET /api/auth/dev/users`
- If desktop sync auth lands on the wrong redirect or cannot continue:
- inspect `/desktop-auth/authorize` query params
- inspect whether `return_to` passed validation
- inspect whether Google callback returned a desktop-auth redirect path
- If Telegram linking fails:
- confirm the user already had a valid session before entering the Telegram flow
- inspect `GET /api/auth/telegram`
- inspect `GET /api/auth/telegram/callback?code=...`
- confirm whether the backend returned a `redirect_url`
- check whether the Telegram account is already linked to this user or another user
- 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