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
- Introduction
- Prerequisites
- Project Setup
- Environment Configuration
- Database Setup with Prisma
- Better Auth Configuration
- Authentication Components
- Server Actions
- Route Protection
- Email Integration
- Testing Your Implementation
- 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
- Navigate to
/signup
- Fill in the registration form
- Verify user is created in database
- Check automatic sign-in works
2. Test Login Flow
- Navigate to
/login
- Use existing credentials
- Verify redirect to dashboard
- Test "Remember me" functionality
3. Test Password Reset
- Go to
/forgot-password
- Enter email address
- Check email is received
- Click reset link
- Verify token validation
- Set new password
4. Test Social Login
- Click "Sign in with Google"
- Complete OAuth flow
- Verify user creation with mapped fields
- Test account linking
5. Test Route Protection
- Try accessing protected routes without authentication
- Verify redirects work correctly
- 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
- Better Auth Official Documentation
- Better Auth GitHub Repository
- Prisma Documentation
- Resend Documentation
- YouTube Tutorial Reference
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