Blog/Tutorials & Step-by-Step/Auth in Next.js with Better Auth
POST
January 25, 2026
LAST UPDATEDJanuary 25, 2026

Auth in Next.js with Better Auth

Implementing secure, session-based authentication in Next.js App Router using the comprehensive Better Auth library.

Tags

Next.jsSecurityAuthentication
Auth in Next.js with Better Auth
3 min read

Auth in Next.js with Better Auth

TL;DR

Better Auth provides a type-safe, database-agnostic authentication solution that integrates seamlessly with Next.js App Router, supporting OAuth, email/password, and MFA out of the box.

Prerequisites

  • Next.js 14+ with App Router and TypeScript
  • A PostgreSQL database (or any supported database)
  • Google and GitHub OAuth credentials (for social login)
  • Basic understanding of Next.js server components and route handlers

Step 1: Install and Configure Better Auth

bash
npm install better-auth

Create the Better Auth configuration file. This is the central configuration that defines your auth behavior.

typescript
// lib/auth.ts
import { betterAuth } from "better-auth";
import { Pool } from "pg";
 
export const auth = betterAuth({
  database: new Pool({
    connectionString: process.env.DATABASE_URL,
  }),
  emailAndPassword: {
    enabled: true,
    minPasswordLength: 8,
  },
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    },
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24, // Refresh session every 24 hours
  },
});

Set up your environment variables:

DATABASE_URL=postgresql://user:password@localhost:5432/myapp
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
BETTER_AUTH_SECRET=your-random-secret-at-least-32-chars
BETTER_AUTH_URL=http://localhost:3000

Step 2: Create the API Route Handler

Better Auth handles all authentication endpoints through a single catch-all route.

typescript
// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
 
export const { GET, POST } = toNextJsHandler(auth);

This single file handles all auth routes automatically:

  • POST /api/auth/sign-up/email -- email/password registration
  • POST /api/auth/sign-in/email -- email/password login
  • GET /api/auth/sign-in/social?provider=google -- OAuth redirect
  • POST /api/auth/sign-out -- session invalidation
  • GET /api/auth/session -- current session check

Step 3: Set Up the Client-Side Auth

Create a client instance that provides React hooks for authentication state.

typescript
// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
 
export const { signIn, signUp, signOut, useSession } = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
});

Build the Sign-Up Form

tsx
// components/SignUpForm.tsx
"use client";
 
import { signUp } from "@/lib/auth-client";
import { useState } from "react";
import { useRouter } from "next/navigation";
 
export function SignUpForm() {
  const router = useRouter();
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);
 
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setLoading(true);
    setError("");
 
    const formData = new FormData(e.currentTarget);
 
    const { error } = await signUp.email({
      email: formData.get("email") as string,
      password: formData.get("password") as string,
      name: formData.get("name") as string,
    });
 
    if (error) {
      setError(error.message || "Something went wrong");
      setLoading(false);
      return;
    }
 
    router.push("/dashboard");
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name</label>
        <input id="name" name="name" type="text" required />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" name="email" type="email" required />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input id="password" name="password" type="password" required minLength={8} />
      </div>
      {error && <p className="text-red-500">{error}</p>}
      <button type="submit" disabled={loading}>
        {loading ? "Creating account..." : "Sign Up"}
      </button>
    </form>
  );
}

Build the Sign-In Form with OAuth

tsx
// components/SignInForm.tsx
"use client";
 
import { signIn } from "@/lib/auth-client";
import { useState } from "react";
import { useRouter } from "next/navigation";
 
export function SignInForm() {
  const router = useRouter();
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);
 
  const handleEmailSignIn = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setLoading(true);
    setError("");
 
    const formData = new FormData(e.currentTarget);
 
    const { error } = await signIn.email({
      email: formData.get("email") as string,
      password: formData.get("password") as string,
    });
 
    if (error) {
      setError(error.message || "Invalid credentials");
      setLoading(false);
      return;
    }
 
    router.push("/dashboard");
  };
 
  const handleOAuthSignIn = async (provider: "google" | "github") => {
    await signIn.social({
      provider,
      callbackURL: "/dashboard",
    });
  };
 
  return (
    <div>
      <form onSubmit={handleEmailSignIn}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" name="email" type="email" required />
        </div>
        <div>
          <label htmlFor="password">Password</label>
          <input id="password" name="password" type="password" required />
        </div>
        {error && <p className="text-red-500">{error}</p>}
        <button type="submit" disabled={loading}>
          {loading ? "Signing in..." : "Sign In"}
        </button>
      </form>
 
      <div className="divider">or continue with</div>
 
      <div className="oauth-buttons">
        <button onClick={() => handleOAuthSignIn("google")}>
          Sign in with Google
        </button>
        <button onClick={() => handleOAuthSignIn("github")}>
          Sign in with GitHub
        </button>
      </div>
    </div>
  );
}

Step 4: Session Management

Client-Side Session Access

Use the useSession hook in client components to access the current user.

tsx
// components/UserMenu.tsx
"use client";
 
import { useSession, signOut } from "@/lib/auth-client";
import { useRouter } from "next/navigation";
 
export function UserMenu() {
  const { data: session, isPending } = useSession();
  const router = useRouter();
 
  if (isPending) return <div>Loading...</div>;
 
  if (!session) {
    return <button onClick={() => router.push("/sign-in")}>Sign In</button>;
  }
 
  return (
    <div className="user-menu">
      <span>Welcome, {session.user.name}</span>
      <button
        onClick={async () => {
          await signOut();
          router.push("/");
        }}
      >
        Sign Out
      </button>
    </div>
  );
}

Server-Side Session Access

Access the session in Server Components and Route Handlers using the auth instance directly.

tsx
// app/dashboard/page.tsx
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
 
export default async function DashboardPage() {
  const session = await auth.api.getSession({
    headers: await headers(),
  });
 
  if (!session) {
    redirect("/sign-in");
  }
 
  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome back, {session.user.name}!</p>
      <p>Email: {session.user.email}</p>
    </div>
  );
}

Step 5: Middleware Protection

Protect entire route groups using Next.js middleware.

typescript
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
 
const protectedRoutes = ["/dashboard", "/settings", "/profile"];
const authRoutes = ["/sign-in", "/sign-up"];
 
export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
 
  const session = await auth.api.getSession({
    headers: request.headers,
  });
 
  // Redirect unauthenticated users away from protected routes
  const isProtected = protectedRoutes.some((route) => pathname.startsWith(route));
  if (isProtected && !session) {
    const signInUrl = new URL("/sign-in", request.url);
    signInUrl.searchParams.set("callbackUrl", pathname);
    return NextResponse.redirect(signInUrl);
  }
 
  // Redirect authenticated users away from auth pages
  const isAuthRoute = authRoutes.some((route) => pathname.startsWith(route));
  if (isAuthRoute && session) {
    return NextResponse.redirect(new URL("/dashboard", request.url));
  }
 
  return NextResponse.next();
}
 
export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*", "/profile/:path*", "/sign-in", "/sign-up"],
};

Step 6: Database Adapter Setup

Better Auth automatically creates the required tables when you first run the application. For production, generate and review migrations.

bash
npx better-auth generate

This creates SQL migration files that you can review and apply to your database. The generated schema includes tables for users, sessions, accounts (for OAuth), and verification tokens.

Custom User Fields

Extend the user model with additional fields:

typescript
// lib/auth.ts
export const auth = betterAuth({
  database: new Pool({
    connectionString: process.env.DATABASE_URL,
  }),
  user: {
    additionalFields: {
      role: {
        type: "string",
        defaultValue: "user",
        required: false,
      },
      bio: {
        type: "string",
        required: false,
      },
    },
  },
  // ... rest of config
});

Step 7: Multi-Factor Authentication

Better Auth includes built-in TOTP-based MFA. Enable it in your configuration.

typescript
// lib/auth.ts
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
 
export const auth = betterAuth({
  // ... base config
  plugins: [
    twoFactor({
      issuer: "MyApp", // Shows in authenticator apps
    }),
  ],
});

Update the client to include the two-factor plugin:

typescript
// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { twoFactorClient } from "better-auth/client/plugins";
 
export const {
  signIn,
  signUp,
  signOut,
  useSession,
  twoFactor,
} = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
  plugins: [twoFactorClient()],
});

Enable MFA for a User

tsx
// components/EnableMFA.tsx
"use client";
 
import { twoFactor } from "@/lib/auth-client";
import { useState } from "react";
 
export function EnableMFA() {
  const [qrCode, setQrCode] = useState<string | null>(null);
  const [verificationCode, setVerificationCode] = useState("");
 
  const handleEnable = async () => {
    const { data } = await twoFactor.enable();
    if (data?.totpURI) {
      // Convert TOTP URI to QR code using a library like qrcode
      setQrCode(data.totpURI);
    }
  };
 
  const handleVerify = async () => {
    const { error } = await twoFactor.verifyTotp({
      code: verificationCode,
    });
    if (!error) {
      alert("MFA enabled successfully!");
    }
  };
 
  return (
    <div>
      {!qrCode ? (
        <button onClick={handleEnable}>Enable Two-Factor Auth</button>
      ) : (
        <div>
          <p>Scan this QR code with your authenticator app:</p>
          <code className="block text-sm break-all">{qrCode}</code>
          <input
            type="text"
            value={verificationCode}
            onChange={(e) => setVerificationCode(e.target.value)}
            placeholder="Enter 6-digit code"
            maxLength={6}
          />
          <button onClick={handleVerify}>Verify</button>
        </div>
      )}
    </div>
  );
}

Putting It All Together

Your authentication system now covers the full spectrum:

  1. Email/password -- registration and login with password hashing handled automatically
  2. OAuth -- Google and GitHub social login with account linking
  3. Session management -- cookie-based sessions accessible in both client and server components
  4. Route protection -- middleware guards for protected and auth-only routes
  5. MFA -- optional TOTP-based second factor for security-sensitive users

Better Auth manages the database schema, session lifecycle, and token rotation. You focus on building the UI and defining which routes need protection.

Next Steps

  • Email verification -- enable the email verification plugin to require users to confirm their email before accessing protected features
  • Password reset -- add the forgot password flow with time-limited reset tokens
  • Rate limiting -- protect auth endpoints from brute force attacks using middleware-based rate limiting
  • Audit logging -- track sign-in events, failed attempts, and MFA changes for security monitoring
  • Role-based access control -- use the custom role field to restrict pages and API routes by user role

FAQ

What is Better Auth and how is it different from NextAuth?

Better Auth is a type-safe, database-agnostic authentication library that provides a more comprehensive and developer-friendly API than NextAuth, with built-in support for session cookies, OAuth, and MFA.

How does Better Auth handle session management?

Better Auth uses standard session cookies for authentication, which are automatically managed and validated on both client and server, making it compatible with Next.js App Router's server components.

Can Better Auth handle multi-factor authentication?

Yes, Better Auth includes built-in support for multi-factor authentication (MFA), allowing you to add TOTP-based or other MFA methods to your authentication flow without additional libraries.

Collaboration

Need help with a project?

Let's Build It

I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI — let's bring your vision to life.

SH

Article Author

Sadam Hussain

Senior Full Stack Developer

Senior Full Stack Developer with over 7 years of experience building React, Next.js, Node.js, TypeScript, and AI-powered web platforms.

Related Articles

How to Add Observability to a Node.js App with OpenTelemetry
Mar 21, 20265 min read
Node.js
OpenTelemetry
Observability

How to Add Observability to a Node.js App with OpenTelemetry

Learn how to instrument a Node.js app with OpenTelemetry for traces, metrics, and logs, and build a practical observability setup for production debugging.

How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js
Mar 21, 20266 min read
Next.js
Node.js
BFF

How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js

A practical guide to building a Backend-for-Frontend with Next.js and Node.js for API aggregation, auth handling, caching, and frontend-specific data shaping.

How I Structure CI/CD for Next.js, Docker, and GitHub Actions
Mar 21, 20265 min read
CI/CD
Next.js
Docker

How I Structure CI/CD for Next.js, Docker, and GitHub Actions

A practical CI/CD blueprint for Next.js apps using Docker and GitHub Actions, including testing, image builds, deployment stages, cache strategy, and release safety.