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 session table).
  • An admin plugin with a role field for role-based access.

Important files

FileRole
src/lib/auth.tsBetter Auth server config (adapter, providers, plugins, email hooks)
src/lib/auth-client.tsReact client (signIn, signUp, signOut, useSession)
src/app/api/auth/[...all]/route.tsCatch-all route handler (toNextJsHandler)
src/proxy.tsCookie-based redirect for protected paths
src/components/auth/LoginForm, OAuthButtons, and related UI
src/lib/db/schema/auth.tsuser, session, account, verification tables

Server setup

src/lib/auth.ts wires Better Auth to the Drizzle adapter and Resend:

  • emailAndPassword.enabled with requireEmailVerification: true — new users must verify before they can log in.
  • emailVerification.sendOnSignUp: true — verification email is sent on registration, delivered through sendEmail (Resend).
  • sendResetPassword — password-reset emails use the ForgotPassword template.
  • socialProviders.github and socialProviders.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 the role field.

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:

src/app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";

export const { GET, POST } = toNextJsHandler(auth);

Auth pages

PathPurpose
/auth/loginEmail/password + OAuth login
/auth/registerRegistration
/auth/register-success"Check your email" confirmation
/auth/confirmEmail verification landing
/auth/forgot-passwordRequest a reset email
/auth/reset-passwordSet 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() from src/lib/auth-client.ts reads 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.role field (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 /dashboard while logged out → redirected to login with a callbackUrl.

Production notes

  • Set a stable, secret BETTER_AUTH_SECRET (rotating it invalidates sessions).
  • Set BETTER_AUTH_URL to 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.

On this page