Authentication
Overview
Authentication is powered by Better Auth and configured entirely through
src/config/auth.config.ts, which reads validated env. The server setup lives
in src/lib/auth/index.ts and the client bindings in src/lib/auth/client.ts.
Auth routes are served by a catch-all at src/app/api/auth/[...all]/route.ts.
Sign-in methods
Every method is a feature flag — turn it on by setting the corresponding env var, off by leaving it unset:
| Method | Flag | Notes |
|---|---|---|
| Email + password | AUTH_EMAIL_PASSWORD_ENABLED | Classic credentials |
| Magic link | AUTH_MAGIC_LINK_ENABLED | Passwordless; users enable it in settings |
| Two-factor (TOTP) | AUTH_TWO_FACTOR_ENABLED | Users enable it in account security |
AUTH_GOOGLE_ENABLED + client ID/secret | OAuth | |
| GitHub | AUTH_GITHUB_ENABLED + client ID/secret | OAuth |
Social providers are only registered when both the client ID and secret are present, so a half-configured provider never appears in the UI.
Adding a social provider
Follow the existing pattern in auth.config.ts — add the provider to the
socialProviders array guarded by its credentials, then register the callback
URL with the provider. No other code changes are needed.
Generic OIDC (Keycloak, Okta, Auth0, Azure AD…)
Beyond the built-in social providers, you can add any OpenID Connect
provider as a single "Continue with …" button, via Better Auth's
genericOAuth plugin. Enable it with env only:
AUTH_OIDC_ENABLED=true
OIDC_PROVIDER_NAME="Company SSO" # the button label
OIDC_ISSUER_URL=https://auth.example.com/realms/my-realm # Keycloak example
OIDC_CLIENT_ID=...
OIDC_CLIENT_SECRET=...
Discovery is derived from the issuer (issuer + /.well-known/openid-configuration),
so any standards-compliant provider works. Register this callback URL in
your provider:
<APP_URL>/api/auth/oauth2/callback/oidc
The button appears on sign-in and sign-up automatically. OIDC users go through the same post-signup provisioning (personal org, terms, etc.) as everyone else.
Email verification
Verification is independently toggleable and can be soft or hard:
emailVerification: {
enabled: env.AUTH_EMAIL_VERIFICATION_ENABLED,
// true → block access until verified
// false → show a banner but allow access
required: env.AUTH_EMAIL_VERIFICATION_REQUIRED,
}
Verification, password-reset, and magic-link emails are sent through the email abstraction using localized React Email templates.
Sessions and rate limiting
- Session cookies live for 30 days by default (
session.maxAge). - Auth endpoints (sign-in, sign-up, password reset, magic link, 2FA verify) are
rate-limited. It defaults to production-only — local dev and E2E suites
sign in repeatedly from one IP and would otherwise trip it. Force it on or off
anywhere with
AUTH_RATE_LIMIT_ENABLED. - Rate limiting is in-memory by default; set
REDIS_URLto make it atomic across instances.
Impersonation
Platform admins can impersonate a user for support. It's part of the admin surface — see Admin panel. Impersonation and other sensitive actions are recorded to the audit log.
Captcha
When TURNSTILE_SECRET_KEY is set, Cloudflare Turnstile captcha activates
automatically on the relevant auth forms. No code change required.
Login history
Every successful sign-in is recorded as a LoginEvent (IP, device, time) and
shown to the user under account → sessions as "Recent sign-ins" — distinct
from active sessions, which disappear on logout. Impersonation sessions are
excluded. Old rows are pruned by the purge-old-login-events
background job.
Ban enforcement
Banned users are redirected to a /banned page. Enforcement runs in
src/proxy.ts on every request (not just on render), with expired bans
auto-cleared.