Runtime Flows
Signup (Path A — Web Application)
Section titled “Signup (Path A — Web Application)”User on tobby.example.com clicks "Sign Up" │ │ POST /api/auth/sign-up/email │ { email, password, name } │ Origin: https://tobby.example.com │ ▼Auth Hook (auth-hooks.ts) intercepts the request │ ├── Converts Fastify request to standard Request ├── Calls Better Auth native handler │ → Creates user + account + session │ → Returns Response with Set-Cookie headers │ ├── Parses the Better Auth response │ → Extracts user.id, email │ ├── Resolves application from request Origin │ → appResolver.resolveFromOrigin(origin) │ → Returns application ID (e.g. "tobby") │ ├── Loads application auth_policy ├── Evaluates SignupPolicyChain: │ AllowedProviders → EmailDomainCheck → SignupPolicyGate │ ├── If policy passes: │ → Creates application_user record │ → Status: "active" (or "pending_approval") │ ├── If policy fails: │ → Deletes the Better Auth user │ → Returns error response │ └── Writes Better Auth response (with Set-Cookie) to reply → Session cookie: better-auth.session_token → Cache cookie: better-auth.session_token.cookie_cacheSignin (Path A — Web Application)
Section titled “Signin (Path A — Web Application)”User on tobby.example.com enters password │ │ POST /api/auth/sign-in/email │ { email, password } │ ▼Auth Hook (auth-hooks.ts) intercepts │ ├── Calls Better Auth native handler │ → Verifies credentials, creates session │ → Returns Set-Cookie with session token │ ├── Parses response → extracts user.id ├── Resolves application from Origin ├── Evaluates SigninPolicyChain: │ ApplicationUserStatus → checks app_user.status │ - "suspended" → returns 403 USER_SUSPENDED │ - "disabled" → returns 403 USER_DISABLED │ - "active" → passes │ └── Writes Better Auth responseOAuth Authorization (Path B — iOS / Isolated Apps)
Section titled “OAuth Authorization (Path B — iOS / Isolated Apps)”iOS App initiates OAuth flow │ │ GET /api/auth/oauth2/authorize │ ?client_id=scli_xxx │ &redirect_uri=com.app://callback │ &code_challenge=xxx │ &code_challenge_method=S256 │ ▼Better Auth oauthProvider plugin processes the request │ ├── No session → redirect to login page (/login) │ User signs in → session created │ → Redirect back to /oauth2/continue │ ├── Session exists → check SSO group isolation (Phase 2) │ Same group → proceed │ Different group → redirect to login │ ├── First-time authorization for this client │ → Redirect to consent page (/consent) │ → User accepts → authorization_code issued │ └── Previously consented (skipConsent) → authorization_code issued directlyToken Exchange
Section titled “Token Exchange”Client exchanges authorization_code for tokens │ │ POST /api/auth/oauth2/token │ grant_type=authorization_code │ code=xxx │ code_verifier=xxx │ client_id=scli_xxx │ client_secret=xxx (confidential client) │ resource=tobby-api (audience) │ ▼Better Auth validates code, PKCE, client credentials │ ├── Issues access_token (JWT, RS256 signed) ├── Issues refresh_token ├── Issues id_token │ └── Response: { access_token, refresh_token, id_token, expires_in }JWT Verification (SaaS Backend)
Section titled “JWT Verification (SaaS Backend)”Each SaaS backend verifies the JWT locally using saas-core’s JWKS endpoint. No callback to saas-core is required.
SaaS backend receives request with Bearer token │ ├── Fetch JWKS: GET https://auth.example.com/api/auth/jwks │ → Cache the public keys (they rotate infrequently) │ ├── Verify with jose (Node) or firebase/php-jwt (PHP): │ jwtVerify(token, jwks, { │ issuer: "https://auth.example.com", │ audience: "<own-audience>", │ algorithms: ["RS256"] │ }) │ ├── If verification passes: │ → Extract sub (user ID) │ → Extract app_user (membership info) │ → Process the request │ └── If verification fails: → Return 401User Self-Service
Section titled “User Self-Service”User requests /api/me/memberships │ ├── extractUserId(req, authRef) │ → Constructs GET /api/auth/get-session with the cookie │ → Calls Better Auth handler │ → Returns user.id from session │ ├── Queries application_user table for user.id │ → Returns all memberships with status, role, profile │ └── Response: { memberships: [...] }