zazzy
Features

Authentication

Email/password, OAuth, magic links, and 2FA via Better Auth.

Authentication is built on Better Auth. It ships email/password sign-in and sign-up, OAuth (GitHub and Google), magic links, email verification, password reset, a profile editor, per-session management, and TOTP two-factor with backup codes — every flow rendered by real form components under features/auth/components.

Core feature

Auth is a core feature (core: true, requires: []). It is never offered in the picker and never pruned — everything else in the app assumes a signed-in user, so the whole dependency graph sits on top of it.

What it does

  • Credentials — email/password sign-up and sign-in with react-hook-form + zod schemas (lib/schemas.ts).
  • OAuth — GitHub and Google, gated by which client-id/secret pairs are present (enabledOAuthProviders), so a provider with no credentials simply doesn't render.
  • Magic links and email verification — single-use links dispatched through the email feature when it's present (see composition).
  • Password reset — forgot/reset flows over the same verification store.
  • Two-factor — TOTP enrollment (TwoFactorSection) and a sign-in challenge (TwoFactorChallengeForm) via Better Auth's twoFactor plugin, with backup codes.
  • Account management — profile edit, change password, and a live sessions list with per-session revoke.

Models it adds

Better Auth's core schema plus the twoFactor plugin store, in prisma/schema.prisma:

ModelPurpose
UserIdentity, email, emailVerified, twoFactorEnabled.
SessionActive sessions (token, expiry, IP/UA).
AccountCredential + OAuth provider links.
VerificationSingle-use tokens for verify / magic-link / reset.
TwoFactorPer-user encrypted TOTP secret + backup codes.

Admin fields prune away

The admin plugin's columns (User.role/banned/banReason/banExpires, Session.impersonatedBy) live on these core models but are wrapped in @feature:admin markers — they prune with the admin feature, not with auth. Core sign-in never reads them.

Env it needs

BETTER_AUTH_SECRET=          # session/token signing secret
BETTER_AUTH_URL=             # canonical app URL (e.g. http://localhost:3000)
GITHUB_CLIENT_ID=            # optional — enables the GitHub button
GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=            # optional — enables the Google button
GOOGLE_CLIENT_SECRET=
AUTH_DISABLE_RATE_LIMIT=     # optional — set in tests/dev to lift the limiter

OAuth vars are optional: a provider only appears when both its id and secret are set.

Composition

Auth has no requires — it is the root of the graph. Its relationships run the other way:

  • orgs requires: ["auth"] — every org membership hangs off a User.
  • email is a soft consumer: when present, auth's verification, magic-link, and reset flows enqueue real templates through enqueueEmail; when email is pruned, those marked call sites fall back to a console transport so the flows still work (just logged, not delivered).

What pruning removes

Nothing — auth is core and cannot be deselected. The only auth-adjacent code that prunes is the marker-wrapped admin surface (the User/Session columns above), which goes when you leave out the admin feature.

On this page