JB logo

Command Palette

Search for a command to run...

yOUTUBE
Blog
Next

Complete Better Auth Integration Guide for Next.js with Prisma - Authentication, Social Login & Password Reset

Build a complete authentication system in Next.js using Better Auth with Prisma. Includes email/password auth, Google OAuth, password reset, custom user fields, role-based access control, and Resend email integration.

Complete Better Auth Integration Guide for Next.js with Prisma

A comprehensive guide to implementing authentication in Next.js using Better Auth with Prisma, including social logins, email/password authentication, password reset functionality, custom user fields, and role-based access control.

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Project Setup
  4. Environment Configuration
  5. Database Setup with Prisma
  6. Better Auth Configuration
  7. Authentication Components
  8. Server Actions
  9. Route Protection
  10. Email Integration
  11. Testing Your Implementation
  12. Additional Resources

Introduction

Better Auth is a modern authentication library for TypeScript applications that provides a complete authentication solution with built-in support for social logins, email/password authentication, session management, and more. This guide will walk you through implementing a full authentication system in Next.js with custom user fields, roles, and password reset functionality.

Key Features Covered:

  • Email/Password authentication
  • Google OAuth integration
  • Password reset via email
  • Custom user fields (firstName, lastName, phone, role)
  • Role-based access control
  • Server-side session management
  • Prisma database integration

Prerequisites

Before starting, ensure you have:

  • Node.js 18+ installed
  • A Next.js 14+ project
  • Basic knowledge of React, TypeScript, and Prisma
  • A PostgreSQL database (we'll use Neon in this example)
  • A Google OAuth app for social login
  • A Resend account for email sending

Project Setup

1. Install Required Dependencies

# Install Better Auth and related packages
npm install better-auth
 
# Install Prisma and database client
npm install prisma @prisma/client
 
# Install additional UI dependencies (if using shadcn/ui)
npm install @hookform/resolvers zod react-hook-form sonner lucide-react
 
# Install Resend for email functionality
npm install resend
 
# Install dev dependencies
npm install -D @types/node

2. Initialize Prisma

pnpm dlx prisma init

Environment Configuration

Create or update your .env.local file with the following variables:

# Better Auth Configuration
BETTER_AUTH_SECRET='1hXr3WliewYVrf1Cp3u30PLRjyW22nWs'
BETTER_AUTH_URL='http://localhost:3000'
 
# Database
DATABASE_URL="postgresql://username:password@host:port/database?sslmode=require"
 
# Email Service (Resend)
RESEND_API_KEY="your_resend_api_key"
 
# Google OAuth (for social login)
GOOGLE_CLIENT_ID="your_google_client_id"
GOOGLE_CLIENT_SECRET="your_google_client_secret"

Note: Generate your BETTER_AUTH_SECRET using: openssl rand -base64 32

Environment Variables Explanation:

  • BETTER_AUTH_SECRET: Used to sign and verify tokens (required for production)
  • BETTER_AUTH_URL: Your application's base URL
  • DATABASE_URL: PostgreSQL connection string
  • RESEND_API_KEY: API key from Resend for sending emails
  • GOOGLE_CLIENT_ID/SECRET: OAuth credentials from Google Cloud Console

Database Setup with Prisma

1. Configure Prisma Schema

Update your prisma/schema.prisma file:

generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id               String    @id @default(cuid())
  firstName        String
  lastName         String
  name             String
  phone            String
  role             UserRole  @default(USER)
  email            String
  emailVerified    Boolean
  phoneVerified    Boolean   @default(false)
  physicalVerified Boolean   @default(false)
  image            String?
  createdAt        DateTime
  updatedAt        DateTime
  sessions         Session[]
  accounts         Account[]
 
  @@unique([email])
  @@unique([phone])
  @@map("user")
}
 
enum UserRole {
  ADMIN
  ASSISTANT_ADMIN
  VENDOR
  USER
}
 
model Session {
  id        String   @id @default(cuid())
  expiresAt DateTime
  token     String
  createdAt DateTime
  updatedAt DateTime
  ipAddress String?
  userAgent String?
  userId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
 
  @@unique([token])
  @@map("session")
}
 
model Account {
  id                    String    @id @default(cuid())
  accountId             String
  providerId            String
  userId                String
  user                  User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  accessToken           String?
  refreshToken          String?
  idToken               String?
  accessTokenExpiresAt  DateTime?
  refreshTokenExpiresAt DateTime?
  scope                 String?
  password              String?
  createdAt             DateTime
  updatedAt             DateTime
 
  @@map("account")
}
 
model Verification {
  id         String    @id @default(cuid())
  identifier String
  value      String
  expiresAt  DateTime
  createdAt  DateTime?
  updatedAt  DateTime?
 
  @@map("verification")
}

2. Create Prisma Client

Create prisma/db.ts:

import { PrismaClient } from "@prisma/client";
 
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};
 
const db = globalForPrisma.prisma ?? new PrismaClient();
 
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
 
export default db;

3. Run Database Migrations

# Generate Prisma client
npx prisma generate
 
# Push schema to database
npx prisma db push
 
# Or create and run migrations
npx prisma migrate dev --name init

Better Auth Configuration

1. Server Configuration (lib/auth.ts)

import { sendEmail } from "@/actions/users";
import db from "@/prisma/db";
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { nextCookies } from "better-auth/next-js";
import { headers } from "next/headers";
 
export const auth = betterAuth({
  database: prismaAdapter(db, {
    provider: "postgresql",
  }),
  emailAndPassword: {
    enabled: true,
    minPasswordLength: 8,
    autoSignIn: true,
    sendResetPassword: async ({ user, url }) => {
      await sendEmail({
        to: user.email,
        subject: "Reset your password",
        url: url,
      });
    },
  },
  account: {
    accountLinking: {
      enabled: true,
    },
  },
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
      mapProfileToUser: (profile) => {
        return {
          firstName: profile.given_name,
          lastName: profile.family_name,
          phone: "1234567890", // Default phone for social logins
          role: "USER",
        };
      },
    },
  },
  user: {
    additionalFields: {
      role: {
        type: "string",
        required: false,
        defaultValue: "USER",
        input: false, // Prevent users from setting their own role
      },
      firstName: {
        type: "string",
        required: true,
      },
      lastName: {
        type: "string",
        required: true,
      },
      phone: {
        type: "string",
        required: true,
      },
    },
  },
  plugins: [nextCookies()],
});
 
export type Session = typeof auth.$Infer.Session;
export type User = typeof auth.$Infer.Session.user;
 
export async function getAuthUser(): Promise<User | null> {
  const session = await auth.api.getSession({
    headers: await headers(),
  });
  const user = session?.user as User;
  return user;
}

2. Client Configuration (lib/auth-client.ts)

import { createAuthClient } from "better-auth/react";
 
export const { signIn, signUp, useSession, signOut } = createAuthClient({
  // baseURL: "http://localhost:3000", // Optional: specify if different from current domain
});

Configuration Explanation:

  • prismaAdapter: Connects Better Auth to your Prisma database
  • emailAndPassword: Enables email/password authentication with custom settings
  • socialProviders: Configures Google OAuth with profile mapping
  • additionalFields: Defines custom user fields beyond the default email/name
  • nextCookies: Plugin for Next.js cookie handling
  • sendResetPassword: Custom function to handle password reset emails

Authentication Components

1. Create Authentication Layout

Create app/(auth)/layout.tsx:

import React, { ReactNode } from "react";
 
export default function AuthLayout({ children }: { children: ReactNode }) {
  return <div>{children}</div>;
}

2. Shared Components

AuthHeader Component (app/(auth)/components/AuthHeader.tsx)

import { AppLogoIcon } from "@/components/global/app-logo-icon";
import Link from "next/link";
import React from "react";
 
export default function AuthHeader({
  title,
  subTitle,
}: {
  title: string;
  subTitle: string;
}) {
  return (
    <div className="flex items-center justify-center flex-col">
      <Link href="/" aria-label="go home">
        <AppLogoIcon className="h-10 fill-current text-black sm:h-12" />
      </Link>
      <h1 className="text-title mb-1 mt-4 text-xl font-semibold">{title}</h1>
      <p className="text-sm">{subTitle}</p>
    </div>
  );
}

SubmitButton Component (app/(auth)/components/SubmitButton.tsx)

import { Button } from "@/components/ui/button";
import { RefreshCcw } from "lucide-react";
import React from "react";
 
export default function SubmitButton({
  isLoading,
  loadingTitle,
  title,
}: {
  isLoading: boolean;
  loadingTitle: string;
  title: string;
}) {
  return (
    <Button className="w-full" type="submit" disabled={isLoading}>
      {isLoading && <RefreshCcw className="animate-spin w-4 h-4 mr-2" />}
      {isLoading ? loadingTitle : title}
    </Button>
  );
}

SocialButtons Component (app/(auth)/components/SocialButtons.tsx)

"use client";
import React from "react";
import { signIn } from "@/lib/auth-client";
import { Button } from "@/components/ui/button";
import { Icons } from "@/components/global/icons";
 
export default function SocialButtons() {
  async function handleSignIn(provider: "google") {
    await signIn.social({
      provider: provider,
    });
  }
 
  return (
    <div className="mt-6 grid grid-cols-1 gap-3">
      <Button
        onClick={() => handleSignIn("google")}
        type="button"
        variant="outline"
      >
        <Icons.google />
        <span>Google</span>
      </Button>
    </div>
  );
}

3. Authentication Pages

Sign Up Page (app/(auth)/signup/page.tsx)

import React from "react";
import Signup from "../components/signup";
 
export default function page() {
  return (
    <div>
      <Signup />
    </div>
  );
}

Login Page (app/(auth)/login/page.tsx)

import React from "react";
import Login from "../components/login";
 
export default function page() {
  return (
    <div>
      <Login />
    </div>
  );
}

Forgot Password Page (app/(auth)/forgot-password/page.tsx)

import React from "react";
import ForgotPassword from "../components/forgot-password";
 
export default function page() {
  return (
    <div>
      <ForgotPassword />
    </div>
  );
}

Reset Password Page (app/(auth)/reset-password/page.tsx)

import React from "react";
import ResetPassword from "../components/reset-password";
import { ErrorCard } from "../components/ErrorCard";
 
export default async function page({
  searchParams,
}: {
  searchParams: Promise<{
    token: string;
  }>;
}) {
  const { token } = await searchParams;
 
  if (!token) {
    return <ErrorCard />;
  }
 
  return (
    <div>
      <ResetPassword token={token} />
    </div>
  );
}

Server Actions

Create User Actions (actions/users.ts)

This file contains all server-side authentication logic using Better Auth's API:

"use server";
 
import PasswordResetEmail from "@/emails/password-reset-email";
import { Resend } from "resend";
import { auth } from "@/lib/auth";
import {
  ForgotPasswordFormValues,
  LoginFormValues,
  RegisterFormValues,
} from "@/types/user.schema";
import { APIError } from "better-auth/api";
 
const resend = new Resend(process.env.RESEND_API_KEY);
const baseUrl = process.env.BETTER_AUTH_URL || "";
 
export async function registerUser(data: RegisterFormValues) {
  try {
    await auth.api.signUpEmail({
      body: {
        email: data.email,
        password: data.password,
        name: `${data.firstName} ${data.lastName}`,
        firstName: data.firstName,
        lastName: data.lastName,
        phone: data.phone,
      },
    });
 
    return {
      success: true,
      data: data,
      error: null,
    };
  } catch (error) {
    if (error instanceof APIError) {
      if (error.status === "UNPROCESSABLE_ENTITY") {
        const errorMsg =
          error.message === "Failed to create user"
            ? "Phone Number is Already Taken"
            : "Email is Already Taken";
        return {
          success: false,
          data: null,
          error: errorMsg,
          status: error.status,
        };
      }
    }
    return {
      success: false,
      data: null,
      error: "Something went wrong",
    };
  }
}
 
export async function loginUser(data: LoginFormValues) {
  try {
    await auth.api.signInEmail({
      body: {
        email: data.email,
        password: data.password,
      },
    });
 
    return {
      success: true,
      data: data,
      error: null,
    };
  } catch (error) {
    if (error instanceof APIError) {
      if (error.status === "UNAUTHORIZED") {
        return {
          success: false,
          data: null,
          error: error.message,
          status: error.status,
        };
      }
    }
    return {
      success: false,
      data: null,
      error: "Something went wrong",
    };
  }
}
 
export async function sendForgotPasswordToken(
  formData: ForgotPasswordFormValues
) {
  try {
    const data = await auth.api.forgetPassword({
      body: {
        email: formData.email,
        redirectTo: `${baseUrl}/reset-password`,
      },
    });
 
    return {
      success: true,
      data: data,
      error: null,
    };
  } catch (error) {
    if (error instanceof APIError) {
      if (error.status === "UNAUTHORIZED") {
        return {
          success: false,
          data: null,
          error: error.message,
          status: error.status,
        };
      }
    }
    return {
      success: false,
      data: null,
      error: "Something went wrong",
    };
  }
}
 
export async function resetPassword(formData: {
  newPassword: string;
  token: string;
}) {
  try {
    const data = await auth.api.resetPassword({
      body: {
        newPassword: formData.newPassword,
        token: formData.token,
      },
    });
 
    return {
      success: true,
      data: data,
      error: null,
    };
  } catch (error) {
    if (error instanceof APIError) {
      if (error.status === "UNAUTHORIZED") {
        return {
          success: false,
          data: null,
          error: error.message,
          status: error.status,
        };
      }
    }
    return {
      success: false,
      data: null,
      error: "Something went wrong",
    };
  }
}
 
type SendMailData = {
  to: string;
  subject: string;
  url: string;
};
 
export async function sendEmail(data: SendMailData) {
  try {
    const { data: resData, error } = await resend.emails.send({
      from: "Ads Market Pro <info@desishub.com>",
      to: data.to,
      subject: data.subject,
      react: PasswordResetEmail({
        userEmail: data.to,
        resetLink: data.url,
        expirationTime: "10 Mins",
      }),
    });
 
    if (error) {
      return {
        success: false,
        error: error,
        data: null,
      };
    }
 
    return {
      success: true,
      error: null,
      data: resData,
    };
  } catch (error) {
    return {
      success: false,
      error: error,
      data: null,
    };
  }
}

Key Functions Explained:

  • registerUser: Creates new user accounts with custom fields
  • loginUser: Handles email/password authentication
  • sendForgotPasswordToken: Initiates password reset process
  • resetPassword: Completes password reset with token validation
  • sendEmail: Sends transactional emails via Resend

Route Protection

1. Basic Route Protection

To protect pages, use the getAuthUser function:

import { getAuthUser } from "@/lib/auth";
import { redirect } from "next/navigation";
 
export default async function DashboardPage() {
  const user = await getAuthUser();
 
  if (!user) {
    redirect("/login");
  }
 
  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      <p>Role: {user.role}</p>
    </div>
  );
}

2. Role-Based Protection

Create a utility for role-based access:

// lib/auth-utils.ts
import { getAuthUser } from "@/lib/auth";
import { redirect } from "next/navigation";
 
export async function requireAuth(allowedRoles?: string[]) {
  const user = await getAuthUser();
 
  if (!user) {
    redirect("/login");
  }
 
  if (allowedRoles && !allowedRoles.includes(user.role)) {
    redirect("/unauthorized");
  }
 
  return user;
}
 
// Usage in pages
export default async function AdminPage() {
  const user = await requireAuth(["ADMIN", "ASSISTANT_ADMIN"]);
 
  return <div>Admin content for {user.name}</div>;
}

3. Client-Side Route Protection

For client components, use the useSession hook:

"use client";
import { useSession } from "@/lib/auth-client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
 
export default function ProtectedComponent() {
  const { data: session, isPending } = useSession();
  const router = useRouter();
 
  useEffect(() => {
    if (!isPending && !session) {
      router.push("/login");
    }
  }, [session, isPending, router]);
 
  if (isPending) return <div>Loading...</div>;
  if (!session) return null;
 
  return <div>Protected content for {session.user.name}</div>;
}

Email Integration

1. Create Email Template

Create emails/password-reset-email.tsx:

import {
  Body,
  Container,
  Head,
  Html,
  Preview,
  Section,
  Text,
  Button,
} from "@react-email/components";
 
interface PasswordResetEmailProps {
  userEmail: string;
  resetLink: string;
  expirationTime: string;
}
 
export default function PasswordResetEmail({
  userEmail,
  resetLink,
  expirationTime,
}: PasswordResetEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Reset your password</Preview>
      <Body style={main}>
        <Container style={container}>
          <Section>
            <Text style={heading}>Password Reset Request</Text>
            <Text style={text}>
              Hello {userEmail},
            </Text>
            <Text style={text}>
              We received a request to reset your password. Click the button below to reset it:
            </Text>
            <Button href={resetLink} style={button}>
              Reset Password
            </Button>
            <Text style={text}>
              This link will expire in {expirationTime} for security reasons.
            </Text>
            <Text style={text}>
              If you didn't request this password reset, please ignore this email.
            </Text>
          </Section>
        </Container>
      </Body>
    </Html>
  );
}
 
const main = {
  backgroundColor: "#f6f9fc",
  fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
};
 
const container = {
  backgroundColor: "#ffffff",
  margin: "0 auto",
  padding: "20px 0 48px",
  marginBottom: "64px",
};
 
const heading = {
  fontSize: "24px",
  letterSpacing: "-0.5px",
  lineHeight: "1.3",
  fontWeight: "400",
  color: "#484848",
  padding: "17px 0 0",
};
 
const text = {
  color: "#484848",
  fontSize: "16px",
  lineHeight: "26px",
};
 
const button = {
  backgroundColor: "#5469d4",
  borderRadius: "4px",
  color: "#fff",
  fontSize: "16px",
  textDecoration: "none",
  textAlign: "center" as const,
  display: "block",
  width: "200px",
  padding: "12px 0",
  margin: "20px 0",
};

2. Set Up Better Auth API Route

Create app/api/auth/[...all]/route.ts:

import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
 
const { GET, POST } = toNextJsHandler(auth);
 
export { GET, POST };

Testing Your Implementation

1. Test Registration Flow

  1. Navigate to /signup
  2. Fill in the registration form
  3. Verify user is created in database
  4. Check automatic sign-in works

2. Test Login Flow

  1. Navigate to /login
  2. Use existing credentials
  3. Verify redirect to dashboard
  4. Test "Remember me" functionality

3. Test Password Reset

  1. Go to /forgot-password
  2. Enter email address
  3. Check email is received
  4. Click reset link
  5. Verify token validation
  6. Set new password

4. Test Social Login

  1. Click "Sign in with Google"
  2. Complete OAuth flow
  3. Verify user creation with mapped fields
  4. Test account linking

5. Test Route Protection

  1. Try accessing protected routes without authentication
  2. Verify redirects work correctly
  3. Test role-based access restrictions

Troubleshooting Common Issues

1. Database Connection Issues

# Test database connection
npx prisma db push
 
# Reset database if needed
npx prisma migrate reset

2. Environment Variables

Make sure all required environment variables are set:

  • BETTER_AUTH_SECRET
  • BETTER_AUTH_URL
  • DATABASE_URL
  • RESEND_API_KEY
  • GOOGLE_CLIENT_ID
  • GOOGLE_CLIENT_SECRET

3. CORS Issues

If you encounter CORS issues, ensure your BETTER_AUTH_URL matches your actual domain.

4. Email Delivery Issues

  • Verify Resend API key is correct
  • Check sender domain is verified in Resend
  • Test with a simple email first

Additional Resources

Conclusion

You now have a complete authentication system with:

✅ Email/password authentication
✅ Google OAuth integration
✅ Password reset functionality
✅ Custom user fields and roles
✅ Route protection
✅ Email notifications
✅ Session management

This implementation provides a solid foundation for any Next.js application requiring authentication. The modular structure makes it easy to extend with additional features like email verification, two-factor authentication, or additional social providers.

Remember to:

  • Keep your environment variables secure
  • Regularly update dependencies
  • Monitor authentication logs
  • Test thoroughly before deploying to production
  • Follow security best practices for password policies and session management