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

Start building your AI team