Policy Engine
Architecture
Section titled “Architecture”The policy engine is a chain-of-responsibility pattern. Each step is a pure
function that receives a PolicyContext and returns a PolicyResult.
If any step returns pass: false, the chain short-circuits and the request
is rejected.
Policy Chains
Section titled “Policy Chains”SignupPolicyChain
Section titled “SignupPolicyChain”Executed after Better Auth creates the user account but before the response is returned. Controls whether the user is allowed to register for this specific application.
AllowedProviders → EmailDomainCheck → SignupPolicyGate| Policy | Checks | Pass condition |
|---|---|---|
| AllowedProviders | The signup method (email, google) is in the application’s allowed_providers list | Provider is allowed |
| EmailDomainCheck | The email domain is not blocked or outside the whitelist | Domain passes filter |
| SignupPolicyGate | The application’s signup_policy setting | Depends on policy type |
Signup policy behaviors:
| Policy | Behavior |
|---|---|
open | Anyone can register. Creates application_user with status=active. |
invite_only | Requires a valid invitation token. Without one, registration is rejected. |
admin_approval | Registration accepted but application_user status set to pending_approval. Admin must approve via the admin console. |
auto_on_first_access | Automatically creates application_user on first access. Used for SSO groups. |
SigninPolicyChain
Section titled “SigninPolicyChain”Executed after Better Auth verifies credentials and creates a session.
ApplicationUserStatus| Policy | Checks | Pass condition |
|---|---|---|
| ApplicationUserStatus | The user’s application_user.status for this application | status is “active” |
If the user is suspended or disabled, signin is rejected even with valid credentials. The user must contact an administrator.
TokenPolicyChain (Phase 2)
Section titled “TokenPolicyChain (Phase 2)”Executed during JWT issuance.
ApplicationUserStatus → CustomClaimsInjectorThe CustomClaimsInjector adds app_user information to the JWT payload,
including the membership id, status, role, and profile.
AuthorizePolicyChain (Phase 2)
Section titled “AuthorizePolicyChain (Phase 2)”Executed during OAuth authorization.
SSOGroupCheck → ConsentPolicy → ApplicationUserGateThe SSOGroupCheck prevents session reuse across different SSO groups.
Implementation
Section titled “Implementation”interface PolicyContext { applicationId: string; application: Application; authPolicy: ApplicationAuthPolicy; userId?: string; email?: string; request?: { origin?: string; method?: string; url?: string };}
interface PolicyResult { pass: boolean; reason?: { code: string; message: string; httpStatus: number }; injectClaims?: Record<string, unknown>;}
type PolicyHandler = (ctx: PolicyContext, db: DB) => Promise<PolicyResult>;Application to Auth Hook
Section titled “Application to Auth Hook”The policy engine is integrated into the auth flow via the Auth Hook
(auth-hooks.ts), which wraps Better Auth’s native handler:
Fastify Request │ ├── toStandardRequest(req) → standard Request object ├── auth.handler(standardReq) → Better Auth processes it │ → Returns Response (success or failure) │ ├── If success: │ → Parse response body │ → Resolve application from origin header │ → Load application auth_policy │ → Evaluate policy chain │ → If pass: create/update application_user │ → If fail: undo Better Auth's work (delete user) │ └── Write Better Auth Response back (with all Set-Cookie headers)