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'stwoFactorplugin, 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:
| Model | Purpose |
|---|---|
User | Identity, email, emailVerified, twoFactorEnabled. |
Session | Active sessions (token, expiry, IP/UA). |
Account | Credential + OAuth provider links. |
Verification | Single-use tokens for verify / magic-link / reset. |
TwoFactor | Per-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 limiterOAuth 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 aUser. - 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.