Skip to main content
Localizia

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:

MethodFlagNotes
Email + passwordAUTH_EMAIL_PASSWORD_ENABLEDClassic credentials
Magic linkAUTH_MAGIC_LINK_ENABLEDPasswordless; users enable it in settings
Two-factor (TOTP)AUTH_TWO_FACTOR_ENABLEDUsers enable it in account security
GoogleAUTH_GOOGLE_ENABLED + client ID/secretOAuth
GitHubAUTH_GITHUB_ENABLED + client ID/secretOAuth

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_URL to 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.