Skip to content

Runtime Flows

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_cache
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 response

OAuth 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 directly
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 }

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 401
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: [...] }