GenerateSaaS

Authentication

Authenticate and authorize users with the @repo/auth Better Auth setup shared across the backend.

@repo/auth wraps Better Auth into a single export const auth (packages/auth/src/config.ts) served through the shared @repo/api backend. Auth is always on - individual capabilities are gated by @repo/config flags and by which OAuth env vars are set.

The Nuxt typed client lives at apps/web-nuxt/app/utils/auth.ts - createAuthClient from better-auth/vue with the organization, twoFactor, magicLink, lastLoginMethod, admin, and passkey plugins, plus the apiKey client plugin (conditional on config.apiKeys?.enabled) and the Stripe and Polar client plugins.

What you get

CapabilityDetails
Email + passwordAlways on. 12-128 char passwords; requireEmailVerification: true blocks sign-in until the address is confirmed.
Magic linkPasswordless link expiring in 15 minutes (expiresIn: 900).
Sessions in RedisStored via secondaryStorage, with a 5-minute signed cookie cache (cookieCache.maxAge); lastActiveAt is stamped on session create and update.
Session revocationDeleting a session evicts it from Redis and fires a notifySessionRevoked notification.
Self-service account deletiondeleteUser.enabled removes the avatar via @repo/storage and emits a user/deleted Inngest event.
Social OAuthGoogle, GitHub, Facebook, Discord, and X buttons, each registering only when its env vars are set.
2FA + passkeysAuthenticator-app TOTP, backup codes, and WebAuthn credentials.
RolesPlatform UserRole and per-membership OrgRole, enforced by server-side guards.
Rate limitingOn by default, backed by Redis. Default 300 req/min per IP, with tighter per-endpoint rules (e.g. 10/min on /sign-in/email, 5/min on /sign-up/email, 3/min on /sign-in/magic-link).

packages/database/src/db/auth.ts is generated by Better Auth (pnpm auth:generate) - never edit it by hand. Add user columns under user.additionalFields in packages/auth/src/config.ts, then regenerate.

Account deletion cascade

Deletion runs beforeDelete (avatar cleanup) then afterDelete (Inngest event + admin notify), and Postgres foreign-key cascades clean up the rest.

Removed on deleteWhere
Avatar@repo/storage via deleteFile
accounts, passkeysDB cascade
members, invitationsDB cascade
twoFactorsDB cascade
SessionsEvicted from Redis - not a Postgres table, not part of the cascade

Explore

Frequently asked questions

Where are sessions stored? In Redis through Better Auth secondaryStorage, with a 5-minute signed cookie cache (cookieCache.maxAge). There is no Postgres sessions table, so deleting a user evicts their sessions from Redis rather than via a DB cascade.

How do I add a column to the user record? Add it under user.additionalFields in packages/auth/src/config.ts, then run pnpm auth:generate to regenerate packages/database/src/db/auth.ts. Never hand-edit the generated file.

How does the Nuxt client read the current session? Use await authClient.useSession(ssrFetch) for SSR pages and gate user-specific UI inside <ClientOnly>. The ssrFetch helper forwards the cookie header and collapses same-origin absolute URLs to relative paths.

Is captcha part of this? It ships disabled ({ enabled: false }) until you turn it on. The Better Auth captcha plugin (Cloudflare Turnstile) registers when config.captcha.enabled is true and TURNSTILE_SECRET_KEY is set, scoped to the /sign-up/email, /sign-in/email, /request-password-reset, and /sign-in/magic-link endpoints.

On this page