Authentication
How the GrowthMRI auth flows work (signup, login, logout, password reset, email verification) and how to use them
Authentication is powered by Better Auth and is already complete: email/password, OAuth (GitHub/Google), email OTP / magic link, organizations, admin, password reset and email verification all ship out of the box. There is no custom auth code to write — this page documents how the existing flows work and how to call them.
- Server config:
src/lib/auth.ts - Browser client:
src/lib/auth-client.ts - API endpoint:
app/api/auth/[...auth]/route.ts(Better Auth catch-all) - UI pages:
app/auth/* - DB models:
prisma/schema/better-auth.prisma(User,Account,Session,Verification)
Passwords are never stored in plain text — Better Auth hashes them (scrypt) and stores the
hash on Account.password for the credential provider.
Flows
Signup
- Page:
/auth/signup→app/auth/signup/sign-up-credentials-form.tsx - Call:
authClient.signUp.email({ email, password, name, image }) - Password match is enforced by
signup.schema.ts(.refine). - On success the user is created, an organization is auto-provisioned (Better Auth
user-create hook in
auth.ts+ theorganizationplugin), and the browser is sent to/orgs. - Email verification is not required to sign in and is not sent on signup
(no
requireEmailVerification/sendOnSignUp).
await authClient.signUp.email({ email, password, name });
Login
- Page:
/auth/signin→sign-in-password-form.tsx - Call:
authClient.signIn.email({ email, password, rememberMe: true }) - Sessions last 20 days and refresh every 7 (
auth.tssession).
Login by OTP / magic link
- Page:
/auth/signin(OTP form) and/auth/signin/otp - Call:
authClient.emailOtp.sendVerificationOtp(...)thenauthClient.signIn.emailOtp(...) - Backed by the
emailOTPplugin inauth.ts.
Logout
- Call:
authClient.signOut()(re-exported fromsrc/lib/auth-client.ts) - Endpoint:
POST /api/auth/sign-out
Password reset (email OTP)
- Page:
/auth/forget-password→forget-password-page.tsx, a 3-step flow:authClient.forgetPassword.emailOtp({ email })— sends a 6-digit OTP.authClient.emailOtp.checkVerificationOtp({ email, type: "forget-password", otp })authClient.emailOtp.resetPassword({ email, otp, password })
- This is the only reset path. The legacy link-based
sendResetPasswordwas removed because no page consumed it.
Email verification
- Trigger (UI): account settings (
/account→edit-profile-form.tsx) shows a Verify email button whenemailVerifiedis false. - Call:
authClient.sendVerificationEmail({ email }) - The email contains a link to
/api/auth/verify-email?token=…. The token is a stateless JWT signed withBETTER_AUTH_SECRET(it is not stored in the DB). Visiting the link flipsUser.emailVerifiedtotrue.
Server-side access
Use the helpers (see .claude/rules/authentication.md):
import { getUser, getRequiredUser } from "@/lib/auth/auth-user";
import { getCurrentOrg, getRequiredCurrentOrg } from "@/lib/organizations/get-org";
const user = await getUser(); // null if signed out
const user = await getRequiredUser(); // throws if signed out
const org = await getRequiredCurrentOrg();
In client components:
import { useSession } from "@/lib/auth-client";
const session = useSession();
Extending auth
Add a custom field to User
- Declare it under
user.additionalFieldsinsrc/lib/auth.ts. - Run
pnpm better-auth:migrateto regenerateprisma/schema/better-auth.prisma. - Apply the migration to the database.
Roles & permissions
- App-level roles use the
adminplugin (User.role,banned, …). - Organization roles/permissions are defined in
src/lib/auth/auth-permissions.ts(ac,roles) and passed to theorganizationplugin inauth.ts.
Required environment variables
Copy .env-template to .env and fill at least:
DATABASE_URL(+DATABASE_URL_UNPOOLED) — PostgresBETTER_AUTH_SECRET— session/JWT signing (also required by the e2e email-verification test)- Email provider (Resend) — needed for reset/verification emails to actually send
- Optional OAuth:
GITHUB_CLIENT_ID/SECRET,GOOGLE_CLIENT_ID/SECRET
Testing
End-to-end tests live in e2e/ and run with pnpm test:e2e:ci (needs a running app + DB):
| Spec | Covers |
|---|---|
e2e/signup.spec.ts | signup, password hashing, org provisioning, password-match guard |
e2e/login.spec.ts | login success + invalid password |
e2e/logout.spec.ts | session ends, protected pages redirect |
e2e/reset-password.spec.ts | full OTP reset; old password rejected |
e2e/email-verification.spec.ts | send + verify via link |
Because e2e has no inbox, helpers in e2e/utils/verification.ts read the reset OTP from the
verification table and mint the verification JWT from BETTER_AUTH_SECRET.
Manual test checklist
- Sign up → redirected to
/orgs/<slug>; user + org exist in DB. - Log out →
/orgsredirects to/auth/signin. - Log in with the same credentials → back to
/orgs. - Forget password → receive OTP email → set a new password → sign in with it.
/account→ Verify email → open the emailed link → badge shows "verified".