Better Auth — Complete Implementation with Next.js and Prisma

Authentication is a fundamental element of any modern web application. This comprehensive guide will show you how to implement a robust authentication system with Better Auth, Prisma and Next.js.
User authentication is often complex to implement correctly. Between password security, email verification, and integration with your database, the challenges are numerous. Better Auth simplifies this process while maintaining high security standards.
Why Choose Better Auth?
Better Auth stands out from other authentication solutions with its flexibility and ease of integration:
- ✅ Minimal configuration - No black magic, just clear and predictable code
- 🚀 Optimal performance - Designed to be fast and fully typed with TypeScript
- 🔧 Compatible with your stack - Integrates perfectly with Prisma, Next.js and other modern frameworks
- 🛡️ Built-in security - Email verification, password reset, and protection against common attacks
- 🔄 Automatic RESTful API - Authentication endpoints generated automatically
1. Installation and Initial Setup
Let's start by installing Better Auth and configuring the development environment.
Installing Dependencies
# Installing Better Auth
npm install better-auth
# Installing Prisma
npm install prisma @prisma/client
npx prisma init
Setting Environment Variables
Create a .env
file in the root of your project and add the following environment variables:
# .env
BETTER_AUTH_SECRET=your_secret_key_here
BETTER_AUTH_URL=http://localhost:3000
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
Prisma Configuration
Create your Prisma schema to manage users and sessions. Here's a basic example to place in prisma/schema.prisma
:
// prisma/schema.prisma
datasource db {
provider = "postgresql" // You can also use "mysql" or "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(cuid())
email String @unique
password String? // Securely stored (hashed)
emailVerified Boolean @default(false)
verificationToken String?
resetToken String?
resetTokenExpiry DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
}
model Session {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
}
After defining your schema, generate the Prisma client and push the schema to your database:
npx prisma generate
npx prisma db push
2. Configuring Better Auth with Prisma
Create an auth.ts
(or auth.js
) file at the root of your project to configure Better Auth with Prisma:
// lib/auth.ts
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";
// Initialize Prisma client
const prisma = new PrismaClient();
// Better Auth configuration
export const auth = betterAuth({
// Using the Prisma adapter
database: prismaAdapter(prisma, {
provider: "postgresql", // or "mysql", "sqlite", etc.
}),
// Email/password authentication configuration
emailAndPassword: {
enabled: true,
// Session duration (14 days by default)
sessionDuration: 14 * 24 * 60 * 60 * 1000,
},
});
This configuration establishes the connection between Better Auth and your database via Prisma, allowing secure management of users and sessions.
3. Email and Password Authentication Configuration
Email and password authentication is the most common method for web applications. Better Auth simplifies its implementation while maintaining a high level of security.
Complete Configuration
Let's extend our configuration to enable all email authentication features:
// lib/auth.ts
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "postgresql",
}),
// Detailed authentication configuration
emailAndPassword: {
enabled: true,
// Password rule customization
passwordRules: {
minLength: 10,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
},
// Custom error messages
errorMessages: {
invalidCredentials: "Invalid email or password",
emailAlreadyInUse: "This email is already in use",
passwordTooWeak: "Password must contain at least 10 characters, including uppercase, lowercase, numbers, and special characters",
},
},
// Email configuration
email: {
// Verification email sending function
sendVerificationEmail: async ({ email, token, user }) => {
const verificationUrl = `${process.env.BETTER_AUTH_URL}/verify-email?token=${token}`;
// Use your preferred email service (SendGrid, Mailjet, etc.)
await sendEmail({
to: email,
subject: "Verify your email address",
html: `
<h1>Welcome to our application!</h1>
<p>Click the link below to verify your email address:</p>
<a href="${verificationUrl}">Verify my email</a>
<p>This link expires in 24 hours.</p>
`,
});
},
// Password reset email sending function
sendPasswordResetEmail: async ({ email, token, user }) => {
const resetUrl = `${process.env.BETTER_AUTH_URL}/reset-password?token=${token}`;
await sendEmail({
to: email,
subject: "Reset your password",
html: `
<h1>Password Reset</h1>
<p>You requested to reset your password. Click the link below:</p>
<a href="${resetUrl}">Reset my password</a>
<p>This link expires in 1 hour.</p>
<p>If you didn't request this reset, please ignore this email.</p>
`,
});
},
// Require email verification to log in
requireVerification: true,
// Verification token validity duration (24 hours)
verificationTokenExpiry: 24 * 60 * 60 * 1000,
},
// Session configuration
session: {
// Session storage strategy (JWT or database)
strategy: "database",
// Session validity duration (14 days)
maxAge: 14 * 24 * 60 * 60 * 1000,
// Automatic session renewal
updateAge: 24 * 60 * 60 * 1000,
},
});
// Email sending function (example with nodemailer)
async function sendEmail({ to, subject, html }) {
// Implement your email sending logic here
// Example with nodemailer:
// const transporter = nodemailer.createTransport({...});
// await transporter.sendMail({ from: 'noreply@example.com', to, subject, html });
}
Automatically Generated API Routes
Better Auth automatically generates the following API endpoints:
POST /api/auth/signup
- New user registrationPOST /api/auth/signin
- Existing user loginPOST /api/auth/signout
- User logoutPOST /api/auth/verify-email
- Email address verificationPOST /api/auth/reset-password
- Password reset requestPOST /api/auth/reset-password-confirm
- Password reset confirmation
4. Integration with Next.js
Integrating Better Auth with Next.js is simple and straightforward. Here's how to configure your Next.js application to use Better Auth.
API Routes Configuration
Create a file at app/api/auth/[...all]/route.ts
(for App Router) or pages/api/auth/[...all].ts
(for Pages Router) and add the following code:
// app/api/auth/[...all]/route.ts (App Router)
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth.handler);
For Pages Router, use this approach:
// pages/api/auth/[...all].ts (Pages Router)
import { toNodeHandler } from "better-auth/node";
import { auth } from "@/lib/auth";
// Disallow body parsing, we will parse it manually
export const config = { api: { bodyParser: false } };
export default toNodeHandler(auth.handler);
With this minimal configuration, Better Auth automatically handles all authentication routes.
Authentication Middleware
To protect your routes and APIs, create an authentication middleware:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from './lib/auth';
export async function middleware(req: NextRequest) {
// List of public paths that don't require authentication
const publicPaths = [
'/',
'/signin',
'/signup',
'/reset-password',
'/verify-email',
];
// Check if current path is public
const path = req.nextUrl.pathname;
if (publicPaths.includes(path) || path.startsWith('/api/auth/')) {
return NextResponse.next();
}
// Check authentication
const session = await auth.api.getSession({
headers: req.headers
});
if (!session) {
// Redirect to login page
const signinUrl = new URL('/signin', req.url);
signinUrl.searchParams.set('callbackUrl', path);
return NextResponse.redirect(signinUrl);
}
// User authenticated, continue
return NextResponse.next();
}
export const config = {
matcher: [
// Apply middleware to all routes except static resources
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};
React Client for Authentication
Better Auth provides React hooks to facilitate client-side integration:
// lib/client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
// The base URL of the server (optional if you're using the same domain)
baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"
});
// Export specific methods if you prefer
export const { signIn, signUp, signOut, useSession } = authClient;
5. Creating Authentication Pages
Now, let's create the authentication pages for our Next.js application.
Login Page
// app/signin/page.tsx
"use client";
import { useState } from 'react';
import { signIn } from '@/lib/client';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
export default function SignIn() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const { data, error } = await signIn.email({
email,
password,
rememberMe: true,
});
if (error) {
setError(error.message);
} else {
router.push('/dashboard');
}
} catch (err) {
setError('An error occurred during sign in');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-md">
<div className="text-center">
<h2 className="mt-6 text-3xl font-bold text-gray-900">Sign In</h2>
<p className="mt-2 text-sm text-gray-600">
Or{' '}
<Link href="/signup" className="font-medium text-indigo-600 hover:text-indigo-500">
create an account
</Link>
</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 text-red-700">
<p>{error}</p>
</div>
)}
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">Email address</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Email address"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">Password</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Password"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
/>
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">
Remember me
</label>
</div>
<div className="text-sm">
<Link href="/forgot-password" className="font-medium text-indigo-600 hover:text-indigo-500">
Forgot your password?
</Link>
</div>
</div>
<div>
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{loading ? 'Signing in...' : 'Sign in'}
</button>
</div>
</form>
</div>
</div>
);
}
Registration Page
// app/signup/page.tsx
"use client";
import { useState } from 'react';
import { signUp } from '@/lib/client';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
export default function SignUp() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const { data, error } = await signUp.email({
email,
password,
name,
});
if (error) {
setError(error.message);
} else {
router.push('/verify-email-sent');
}
} catch (err) {
setError('An error occurred during sign up');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-md">
<div className="text-center">
<h2 className="mt-6 text-3xl font-bold text-gray-900">Create an account</h2>
<p className="mt-2 text-sm text-gray-600">
Or{' '}
<Link href="/signin" className="font-medium text-indigo-600 hover:text-indigo-500">
sign in to your account
</Link>
</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 text-red-700">
<p>{error}</p>
</div>
)}
<div className="rounded-md shadow-sm space-y-4">
<div>
<label htmlFor="name" className="sr-only">Full name</label>
<input
id="name"
name="name"
type="text"
autoComplete="name"
required
value={name}
onChange={(e) => setName(e.target.value)}
className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Full name"
/>
</div>
<div>
<label htmlFor="email" className="sr-only">Email address</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Email address"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">Password</label>
<input
id="password"
name="password"
type="password"
autoComplete="new-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Password"
/>
</div>
</div>
<div>
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{loading ? 'Creating account...' : 'Sign up'}
</button>
</div>
</form>
</div>
</div>
);
}
6. Security and Best Practices
To ensure the security of your authentication system, follow these best practices:
CSRF Protection
Better Auth automatically integrates CSRF (Cross-Site Request Forgery) protection for all authentication routes. No additional configuration is necessary.
Rate Limiting
To protect against brute force attacks, add rate limiting:
// middleware.ts
import { rateLimit } from 'express-rate-limit';
// Limit login attempts
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 maximum attempts
message: { error: 'Too many login attempts. Please try again later.' },
});
// Apply limiter to authentication routes
app.use('/api/auth/signin', loginLimiter);
Environment Variables
Always store sensitive information in environment variables:
# .env.local
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
BETTER_AUTH_SECRET="your-very-secure-secret"
BETTER_AUTH_URL="http://localhost:3000"
EMAIL_SERVER="smtp://user:pass@smtp.example.com:587"
EMAIL_FROM="noreply@example.com"
7. Using Sessions in Server Components
Better Auth makes it easy to access user sessions in Server Components:
// app/profile/page.tsx
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function ProfilePage() {
const session = await auth.api.getSession({
headers: headers()
});
if (!session) {
redirect('/signin');
}
return (
<div className="container mx-auto p-6">
<h1 className="text-2xl font-bold mb-4">Profile</h1>
<div className="bg-white shadow rounded-lg p-6">
<div className="flex items-center space-x-4">
{session.user.image && (
<img
src={session.user.image}
alt={session.user.name || 'User'}
className="h-16 w-16 rounded-full"
/>
)}
<div>
<h2 className="text-xl font-medium">{session.user.name}</h2>
<p className="text-gray-500">{session.user.email}</p>
</div>
</div>
</div>
</div>
);
}
Conclusion
Better Auth offers a complete and flexible solution for authentication in modern applications. By integrating it with Prisma and Next.js, you benefit from a robust, secure, and easy-to-maintain system.
Here are the main advantages of this approach:
- 🔒 High-level security - Password hashing, CSRF protection, and email verification
- 🚀 Optimal performance - Direct integration with your database via Prisma
- 🔄 Maximum flexibility - Complete customization of the authentication flow
- 📱 Smooth user experience - Modern and responsive authentication pages
To go further, explore Better Auth's advanced features such as two-factor authentication (2FA), social authentication (Google, GitHub, etc.), and role-based authorization strategies.
🔗 Complete Better Auth documentation
🔗 Next.js integration examples
Feel free to join the Better Auth community to ask questions and share your experiences!