Authentication
Set up Better Auth: email, OAuth, and sessions.
Authentication is integrated end to end with Better Auth: email/password with required verification, password reset, GitHub and Google OAuth, sessions, and an admin plugin for roles.
What it provides
- Email/password registration with required email verification.
- Password reset by email.
- GitHub and Google OAuth, with account linking.
- Database-backed sessions (the
sessiontable). - An admin plugin with a
rolefield for role-based access.
Important files
| File | Role |
|---|---|
src/lib/auth.ts | Better Auth server config (adapter, providers, plugins, email hooks) |
src/lib/auth-client.ts | React client (signIn, signUp, signOut, useSession) |
src/app/api/auth/[...all]/route.ts | Catch-all route handler (toNextJsHandler) |
src/proxy.ts | Cookie-based redirect for protected paths |
src/components/auth/ | LoginForm, OAuthButtons, and related UI |
src/lib/db/schema/auth.ts | user, session, account, verification tables |
Server setup
src/lib/auth.ts wires Better Auth to the Drizzle adapter and Resend:
emailAndPassword.enabledwithrequireEmailVerification: true— new users must verify before they can log in.emailVerification.sendOnSignUp: true— verification email is sent on registration, delivered throughsendEmail(Resend).sendResetPassword— password-reset emails use theForgotPasswordtemplate.socialProviders.githubandsocialProviders.google— read their client ID/secret from the environment.account.accountLinking.enabled: true— links OAuth accounts to an existing user by email.plugins: [admin()]— adds the admin plugin and therolefield.
Verification is required by default. If RESEND_API_KEY is not configured,
the verification email can't send and new accounts can't log in. Configure
Email or relax requireEmailVerification in
src/lib/auth.ts for local testing.
The route handler
All auth endpoints are served by one catch-all route:
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);Auth pages
| Path | Purpose |
|---|---|
/auth/login | Email/password + OAuth login |
/auth/register | Registration |
/auth/register-success | "Check your email" confirmation |
/auth/confirm | Email verification landing |
/auth/forgot-password | Request a reset email |
/auth/reset-password | Set a new password |
OAuth providers
src/lib/auth.ts always registers GitHub and Google, and OAuthButtons
renders a button for each. The provider credentials come from
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET and GOOGLE_CLIENT_ID /
GOOGLE_CLIENT_SECRET — see Environment Variables.
When registering a provider, set the callback URL to
{BETTER_AUTH_URL}/api/auth/callback/{provider}, e.g.
http://localhost:3000/api/auth/callback/github.
If you don't configure a provider, remove or hide its button in
src/components/auth/OAuthButtons.tsx (and the provider entry in
src/lib/auth.ts). A button backed by empty credentials will fail at the
provider.
Sessions and roles
-
Client:
useSession()fromsrc/lib/auth-client.tsreads the current session in Client Components. -
Server: read the session authoritatively in route handlers and Server Components:
const session = await auth.api.getSession({ headers: await headers() }); -
Admin: the
user.rolefield (default"user") gates admin access. The admin users API enforces it:src/app/api/admin/users/route.ts if (session.user.role !== "admin") { return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 }); }
Navigation visibility is not authorization. src/proxy.ts only checks for
the presence of a session cookie to redirect protected paths
(/dashboard, /admin, /setting, /account) — it does not validate the
session or the role. Every sensitive API handler and Server Component must
still call auth.api.getSession and check the role itself, as the admin users
API does. Hiding a menu item is a UX detail, not a security boundary.
Verify
- Register a new account at
/auth/login→ register. - Receive and click the verification email (requires Resend).
- Log in with the verified account.
- Sign in with GitHub and Google (if configured).
- Request a password reset and set a new password.
- Log out.
- Visit
/dashboardwhile logged out → redirected to login with acallbackUrl.
Production notes
- Set a stable, secret
BETTER_AUTH_SECRET(rotating it invalidates sessions). - Set
BETTER_AUTH_URLto your canonical HTTPS URL and register matching OAuth callback URLs. - Keep authoritative session/role checks in every protected API and mutation —
do not rely on
proxy.ts.
