# Tenant & OAuth Flow This document describes how tenant resolution, Better Auth instances, and dashboard redirects tie together across the platform. ## Request Bootstrap 1. `RequestContextMiddleware` runs first on every request. - Calls `TenantContextResolver.resolve()` to populate `HttpContext.tenant`. - Calls `AuthProvider.getAuth()` so downstream handlers reuse the tenant-aware Better Auth instance. 2. `TenantContextResolver` inspects `x-forwarded-host`, `origin`, and `host` headers. - Extracts a slug via `tenant-host.utils.ts`. - Loads the tenant aggregate; when a subdomain hits `/auth` or `/api/auth` for the first time, it auto-provisions a real tenant record with `status = "pending"` so auth flows have a fully-qualified tenant id. - Always stores the original `requestedSlug` so downstream services know which workspace was requested. ## Auth Provider - `AuthProvider` caches Better Auth instances by `protocol://host::slug::settings-hash`. - The slug priority is: 1. `HttpContext.tenant.requestedSlug` 2. `HttpContext.tenant.slug` 3. Derived from the host (when the context slug is still the placeholder). - Redirect URIs are fixed to `/api/auth/callback/:provider` (Google-friendly). - Tenant routing is encoded into the OAuth `state` value (HMAC wrapped) so the gateway can forward callbacks to the right tenant without dynamic redirect URIs. - Because the requested slug participates in the cache key, the same Better Auth instance handles both the `/auth/social` request and the gateway callback, preserving OAuth state. ## System Settings & Gateway - System settings only manage: - Registration flags - Base domain - OAuth gateway URL - Provider credentials - No per-provider redirect URIs are stored; every provider points to the centralized gateway. - `/auth/social/providers` reflects the enabled providers for the UI. ## Session Payload `GET /auth/session` returns: ```ts { user: BetterAuthUser, session: BetterAuthSession, tenant: { id: string, slug: string | null, // Effective slug (requested slug if present, otherwise actual) isPlaceholder: boolean } | null } ``` - When a tenant is still provisioning (`status = "pending"`), `tenant.slug` still holds the requested subdomain, `isPlaceholder` is `true`, and the dashboard stays on the onboarding surface. - Consumers simply check `tenant.isPlaceholder` to know whether they are in onboarding. ## Dashboard Behavior - **Welcome flow** (`/platform/welcome`): - Locks the slug input to `window.location.hostname` via `getTenantSlugFromHost`. - Shows `TenantMissing` or `TenantRestricted` pages, but relies on backend redirects for actual auth. - **Hooks (`usePageRedirect`)**: - If `tenant` is null or `isPlaceholder`, stay on welcome routes. - If `tenant.slug` exists and differs from the current host, sign out placeholder cookies and `window.location.replace(buildTenantUrl(tenant.slug))`. - Superadmin routes are gated separately. ## OAuth Happy Path 1. User opens `https://slug.example.com` (maybe not provisioned yet). 2. Resolver sets `requestedSlug = "slug"`, but tenant aggregate may still be the placeholder. 3. User clicks “Sign in with GitHub” → `/auth/social` uses `requestedSlug` and redirects via the OAuth gateway. 4. Gateway forwards the callback to `https://slug.example.com/api/auth/callback/github`. 5. Resolver again sets `requestedSlug = "slug"`; Better Auth instance cache hits, so `state` matches. 6. `/auth/session` returns `{ tenant: { slug: "slug", isPlaceholder: true } }` while the workspace is pending → dashboard stays on welcome, no cross-subdomain jump. 7. Once the onboarding API marks the tenant `active`, future sessions have `isPlaceholder: false`, and `usePageRedirect` ensures we land on the actual workspace subdomain. ## Key Guarantees - Only a single `tenant.slug` crosses the API boundary; there are no ambiguous fields. - Placeholder detection is a boolean (`isPlaceholder`). - Better Auth instances survive OAuth handshakes regardless of tenant provisioning state.