saasprokit
Authentication

Authentication Overview

Architecture of the Better Auth integration: the auth instance, plugins, packages, and how the auth pages talk to it

Authentication is built on Better Auth rather than hand-rolled session logic. A single auth instance is configured in packages/auth/server.ts, mounted at the catch-all route /api/auth/[...all], and backed by the Prisma/PostgreSQL adapter. Every flow — email/password, social, magic link, email OTP, two-factor, and password reset — runs through that one instance. The auth pages reach it server-side: a form dispatches a server action, and the action calls auth.api.* directly. The typed authClient (packages/auth/client.ts) matches the server plugin list and exports types. Auth pages and session reads do not call it at runtime — forms use auth.api.* in server actions; session helpers are getServerSession / requireAuth.

Ownership: who owns what

The integration is split between the @repo/auth package (the Better Auth server + client instances and the RBAC definitions) and the auth pages in the web app (apps/web/app/[locale]/auth/*), which are thin UIs over colocated server actions.

@repo/auth package (packages/auth/)

FileResponsibility
server.tsThe betterAuth({...}) instance — providers, plugins, sessions, rate limits, email hooks. The source of truth for what auth can do.
client.tsThe createAuthClient({...}) instance and the re-exported method bindings (signIn, signUp, useSession, twoFactor, organization, subscription, emailOtp, admin).
permissions.tsDual RBAC: platform admin roles and organization roles, plus the access-control statements. See Sessions & Route Protection.
keys.tsThe Zod-validated env schema (BETTER_AUTH_SECRET, OAuth keys, Stripe keys, MAX_ORGS_PER_USER). Throws on startup if a required var is missing or malformed.
stripe-client.ts, plans.ts, stripe-hooks.ts, billing-authorize.tsBilling — see the Payment section.

Package exports (package.json):

SubpathWhat you get
@repo/authPermission constants + roles + the auth instance re-export
@repo/auth/serverServer-only auth instance and the Auth type
@repo/auth/clientauthClient and the bound method exports (client-only, "use client")
@repo/auth/permissionsADMIN_ROLES, ORG_ROLES, ac/orgAc, role definitions, PERMISSIONS, RESOURCES
@repo/auth/keysThe env schema (keys())
@repo/auth/plansPlan catalog / Stripe plan builders — see Payment
@repo/auth/stripe-clientStripe SDK instance — see Payment
@repo/auth/guardsBilling plan-change guards — see Payment
@repo/auth/helpersSubscription/usage helpers + UsageStats — see Payment
@repo/auth/ip-headersTrusted IP header names for rate limiting

The auth pages (apps/web/app/[locale]/auth/)

Most pages follow the same "thin page, fat client" shape: a Server Component (page.tsx) awaits params, loads the localized error dictionary, and renders a "use client" form component colocated in a components/ subdir next to the page. Each form keeps react-hook-form for client-side validation and submits through a server action driven by useActionState; the action re-validates with safeParse and returns field/root errors as state (no design-system split, no authClient in the browser).

// page.tsx — the pattern, repeated for most flows
export default async function ForgotPasswordPage({ params }: ForgotPasswordPageProps) {
  const { locale } = await params;
  const dict = await getDictionary(locale);
  return <ForgotPasswordForm errors={dict.errors.action} />;
}

Sign-in is the one exception: its page.tsx is a Server Component that hosts the <Tabs> shell directly (reading ?tab= to pre-select a tab and ?callbackUrl= for the post-login destination) and renders three per-method client components rather than a single client.

RoutePage → ClientFlow
/auth/sign-insign-in/page.tsxcomponents/{sign-in-form,magic-link-sign-in,email-otp-sign-in}.tsxPassword, social, magic link, email OTP (page hosts the tabs)
/auth/sign-upsign-up/page.tsxcomponents/sign-up-form.tsxRegister with email/password or social
/auth/verify-emailverify-email/page.tsxcomponents/verify-email-card.tsx"Check your email" + resend
/auth/forgot-passwordforgot-password/page.tsxcomponents/forgot-password-form.tsxRequest a reset email
/auth/reset-passwordreset-password/page.tsxcomponents/reset-password-form.tsxSet a new password from a token
/auth/2fa2fa/page.tsxcomponents/two-factor-form.tsxEnter the TOTP second factor

Shared pieces (AuthCard, SocialProviders, and the zod schemas) live at auth/components/ and auth/schemas.ts — there is no design-system auth-molecule layer.

Enabled methods & plugins

The auth instance enables a wide surface through the plugin array in server.ts. Each server plugin has a matching client plugin in client.ts.

CapabilityServer configClient plugin
Email & passwordemailAndPassword (verification required)built-in signIn.email / signUp.email
Social (Google, GitHub)socialProvidersbuilt-in signIn.social
Magic linkmagicLink() pluginmagicLinkClient()
Email OTPemailOTP() pluginemailOTPClient()
Two-factortwoFactor() plugin (TOTP + email OTP)twoFactorClient()
Multi-sessionmultiSession()multiSessionClient()
Platform admin RBACadmin({ ac, roles })adminClient({ ac, roles })
Organizations & teamsorganization({ ac, roles })organizationClient({ ac, roles })
Stripe billingstripe({...}) (conditional)stripeClient({ subscription: true })
i18n@better-auth/i18n i18n({...})i18nClient()
Next.js cookiesnextCookies()must stay last in the server plugins array

See Configuration for the full breakdown of each option.

Architecture

Browser (React) Next.js server External useActionState dispatch verification / reset / OTP / magic link OAuth redirect getSessionCookie() — cookie read only, no BA call auth.api.* email links · OAuth callbacks getSession() "Auth pagesapps/web/app/[locale middleware.tscookie-presence route gate + CSP nonce Colocated server actionsauth/*/actions.ts "/api/auth/[...all Better Auth instancepackages/auth/server.ts lib/auth/auth.tsgetServerSession / requireAuth user · session · accountverification · twoFactor@repo/database @repo/emailResend / MailDev Google · GitHub

Outbound: an auth page dispatches its colocated server action, which calls auth.api.* in-process — no browser fetch to /api/auth/* is involved. That route still matters for traffic the server can't originate: the links in verification, reset and magic-link emails, and OAuth provider callbacks. The Better Auth instance runs the flow, persists via Prisma, and sends transactional email through @repo/email. Server Components read the resulting session through the cached helpers in lib/auth/auth.ts (auth.api.getSession — session-verified, cookie-cache eligible); the edge middleware.ts only gates on cookie presence before that — see Sessions & Route Protection.

Because auth.api.* bypasses Better Auth's HTTP-path rate limiter (which only runs on /api/auth/*), brute-forceable actions re-apply enforceRateLimit server-side — see apps/web/lib/auth/rate-limit.ts.

Persistence

Better Auth's Prisma adapter reads and writes a small set of tables (packages/database/prisma/schema.prisma):

ModelHolds
UserIdentity + emailVerified, twoFactorEnabled, role, banned, profile fields
SessionActive sessions (token, expiresAt, ipAddress, userAgent, activeOrganizationId)
AccountCredentials and linked OAuth accounts (hashed password, provider tokens)
VerificationShort-lived tokens for email verification, magic links, OTP, password reset
TwoFactorPer-user TOTP secret + backupCodes
RateLimitRate-limit counters (storage is set to database)

Doc pages in this section

PageWhat it covers
ConfigurationThe betterAuth({...}) instance: providers, plugins, sessions, rate limiting, email hooks, and the env schema.
Sign Up & Email VerificationRegistration via email/password or social, and the mandatory email-verification gate.
Sign InThe tabbed sign-in page: password, social, magic link, and email OTP — plus the 2FA hand-off.
Two-Factor AuthenticationThe TOTP/email-OTP second factor, the server-action handoff from password sign-in, and the /auth/2fa step.
Password ResetForgot-password → reset-password, token handling, and session revocation.
Sessions & Route ProtectionSession lifetime, the cookie cache, the middleware.ts gate, and the cached server helpers.

Source files

  • packages/auth/server.ts — the betterAuth({...}) instance and plugin array
  • packages/auth/client.tsauthClient and the bound method exports
  • packages/auth/permissions.ts — admin + org RBAC (see Sessions & Route Protection)
  • packages/auth/keys.ts — env schema
  • packages/utils/auth.ts — timing constants (session/OTP/magic-link expiry)
  • apps/web/app/api/auth/[...all]/route.ts — the Better Auth Next.js handler
  • apps/web/app/[locale]/auth/* — the auth pages and their clients
  • apps/web/lib/auth/auth.tsgetServerSession, requireAuth, hasOrgPermission
  • apps/web/middleware.ts — edge route gating (cookie-presence check only) and CSP nonce

On this page