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
1# Installing Better Auth
2npm install better-auth
3
4# Installing Prisma
5npm install prisma @prisma/client
6npx prisma init
Setting Environment Variables
Create a .env
file in the root of your project and add the following environment variables:
1# .env
2BETTER_AUTH_SECRET=your_secret_key_here
3BETTER_AUTH_URL=http://localhost:3000
4DATABASE_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
:
1// prisma/schema.prisma
2datasource db {
3 provider = "postgresql" // You can also use "mysql" or "sqlite"
4 url = env("DATABASE_URL")
5}
6
7generator client {
8 provider = "prisma-client-js"
9}
10
11model User {
12 id String @id @default(cuid())
13 email String @unique
14 password String? // Securely stored (hashed)
15 emailVerified Boolean @default(false)
16 verificationToken String?
17 resetToken String?
18 resetTokenExpiry DateTime?
19 createdAt DateTime @default(now())
20 updatedAt DateTime @updatedAt
21 sessions Session[]
22}
23
24model Session {
25 id String @id @default(cuid())
26 userId String
27 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
28 token String @unique
29 expiresAt DateTime
30 createdAt DateTime @default(now())
31}
After defining your schema, generate the Prisma client and push the schema to your database:
1npx prisma generate
2npx 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:
1// lib/auth.ts
2import { betterAuth } from "better-auth";
3import { prismaAdapter } from "better-auth/adapters/prisma";
4import { PrismaClient } from "@prisma/client";
5
6// Initialize Prisma client
7const prisma = new PrismaClient();
8
9// Better Auth configuration
10export const auth = betterAuth({
11 // Using the Prisma adapter
12 database: prismaAdapter(prisma, {
13 provider: "postgresql", // or "mysql", "sqlite", etc.
14 }),
15
16 // Email/password authentication configuration
17 emailAndPassword: {
18 enabled: true,
19 // Session duration (14 days by default)
20 sessionDuration: 14 * 24 * 60 * 60 * 1000,
21 },
22});
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:
1// lib/auth.ts
2import { betterAuth } from "better-auth";
3import { prismaAdapter } from "better-auth/adapters/prisma";
4import { PrismaClient } from "@prisma/client";
5
6const prisma = new PrismaClient();
7
8export const auth = betterAuth({
9 database: prismaAdapter(prisma, {
10 provider: "postgresql",
11 }),
12
13 // Detailed authentication configuration
14 emailAndPassword: {
15 enabled: true,
16 // Password rule customization
17 passwordRules: {
18 minLength: 10,
19 requireUppercase: true,
20 requireLowercase: true,
21 requireNumbers: true,
22 requireSpecialChars: true,
23 },
24 // Custom error messages
25 errorMessages: {
26 invalidCredentials: "Invalid email or password",
27 emailAlreadyInUse: "This email is already in use",
28 passwordTooWeak: "Password must contain at least 10 characters, including uppercase, lowercase, numbers, and special characters",
29 },
30 },
31
32 // Email configuration
33 email: {
34 // Verification email sending function
35 sendVerificationEmail: async ({ email, token, user }) => {
36 const verificationUrl = `${process.env.BETTER_AUTH_URL}/verify-email?token=${token}`;
37
38 // Use your preferred email service (SendGrid, Mailjet, etc.)
39 await sendEmail({
40 to: email,
41 subject: "Verify your email address",
42 html: `
43 <h1>Welcome to our application!</h1>
44 <p>Click the link below to verify your email address:</p>
45 <a href="${verificationUrl}">Verify my email</a>
46 <p>This link expires in 24 hours.</p>
47 `,
48 });
49 },
50
51 // Password reset email sending function
52 sendPasswordResetEmail: async ({ email, token, user }) => {
53 const resetUrl = `${process.env.BETTER_AUTH_URL}/reset-password?token=${token}`;
54
55 await sendEmail({
56 to: email,
57 subject: "Reset your password",
58 html: `
59 <h1>Password Reset</h1>
60 <p>You requested to reset your password. Click the link below:</p>
61 <a href="${resetUrl}">Reset my password</a>
62 <p>This link expires in 1 hour.</p>
63 <p>If you didn't request this reset, please ignore this email.</p>
64 `,
65 });
66 },
67
68 // Require email verification to log in
69 requireVerification: true,
70
71 // Verification token validity duration (24 hours)
72 verificationTokenExpiry: 24 * 60 * 60 * 1000,
73 },
74
75 // Session configuration
76 session: {
77 // Session storage strategy (JWT or database)
78 strategy: "database",
79
80 // Session validity duration (14 days)
81 maxAge: 14 * 24 * 60 * 60 * 1000,
82
83 // Automatic session renewal
84 updateAge: 24 * 60 * 60 * 1000,
85 },
86});
87
88// Email sending function (example with nodemailer)
89async function sendEmail({ to, subject, html }) {
90 // Implement your email sending logic here
91 // Example with nodemailer:
92 // const transporter = nodemailer.createTransport({...});
93 // await transporter.sendMail({ from: 'noreply@example.com', to, subject, html });
94}
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:
1// app/api/auth/[...all]/route.ts (App Router)
2import { auth } from "@/lib/auth";
3import { toNextJsHandler } from "better-auth/next-js";
4
5export const { GET, POST } = toNextJsHandler(auth.handler);
For Pages Router, use this approach:
1// pages/api/auth/[...all].ts (Pages Router)
2import { toNodeHandler } from "better-auth/node";
3import { auth } from "@/lib/auth";
4
5// Disallow body parsing, we will parse it manually
6export const config = { api: { bodyParser: false } };
7
8export 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:
1// middleware.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { auth } from './lib/auth';
4
5export async function middleware(req: NextRequest) {
6 // List of public paths that don't require authentication
7 const publicPaths = [
8 '/',
9 '/signin',
10 '/signup',
11 '/reset-password',
12 '/verify-email',
13 ];
14
15 // Check if current path is public
16 const path = req.nextUrl.pathname;
17 if (publicPaths.includes(path) || path.startsWith('/api/auth/')) {
18 return NextResponse.next();
19 }
20
21 // Check authentication
22 const session = await auth.api.getSession({
23 headers: req.headers
24 });
25
26 if (!session) {
27 // Redirect to login page
28 const signinUrl = new URL('/signin', req.url);
29 signinUrl.searchParams.set('callbackUrl', path);
30 return NextResponse.redirect(signinUrl);
31 }
32
33 // User authenticated, continue
34 return NextResponse.next();
35}
36
37export const config = {
38 matcher: [
39 // Apply middleware to all routes except static resources
40 '/((?!_next/static|_next/image|favicon.ico).*)',
41 ],
42};
React Client for Authentication
Better Auth provides React hooks to facilitate client-side integration:
1// lib/client.ts
2import { createAuthClient } from "better-auth/react";
3
4export const authClient = createAuthClient({
5 // The base URL of the server (optional if you're using the same domain)
6 baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"
7});
8
9// Export specific methods if you prefer
10export const { signIn, signUp, signOut, useSession } = authClient;
5. Creating Authentication Pages
Now, let's create the authentication pages for our Next.js application.
Login Page
1// app/signin/page.tsx
2"use client";
3
4import { useState } from 'react';
5import { signIn } from '@/lib/client';
6import Link from 'next/link';
7import { useRouter } from 'next/navigation';
8
9export default function SignIn() {
10 const [email, setEmail] = useState('');
11 const [password, setPassword] = useState('');
12 const [error, setError] = useState('');
13 const [loading, setLoading] = useState(false);
14 const router = useRouter();
15
16 const handleSubmit = async (e) => {
17 e.preventDefault();
18 setError('');
19 setLoading(true);
20
21 try {
22 const { data, error } = await signIn.email({
23 email,
24 password,
25 rememberMe: true,
26 });
27
28 if (error) {
29 setError(error.message);
30 } else {
31 router.push('/dashboard');
32 }
33 } catch (err) {
34 setError('An error occurred during sign in');
35 } finally {
36 setLoading(false);
37 }
38 };
39
40 return (
41 <div className="min-h-screen flex items-center justify-center bg-gray-50">
42 <div className="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-md">
43 <div className="text-center">
44 <h2 className="mt-6 text-3xl font-bold text-gray-900">Sign In</h2>
45 <p className="mt-2 text-sm text-gray-600">
46 Or{' '}
47 <Link href="/signup" className="font-medium text-indigo-600 hover:text-indigo-500">
48 create an account
49 </Link>
50 </p>
51 </div>
52
53 <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
54 {error && (
55 <div className="bg-red-50 border-l-4 border-red-500 p-4 text-red-700">
56 <p>{error}</p>
57 </div>
58 )}
59
60 <div className="rounded-md shadow-sm -space-y-px">
61 <div>
62 <label htmlFor="email" className="sr-only">Email address</label>
63 <input
64 id="email"
65 name="email"
66 type="email"
67 autoComplete="email"
68 required
69 value={email}
70 onChange={(e) => setEmail(e.target.value)}
71 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"
72 placeholder="Email address"
73 />
74 </div>
75 <div>
76 <label htmlFor="password" className="sr-only">Password</label>
77 <input
78 id="password"
79 name="password"
80 type="password"
81 autoComplete="current-password"
82 required
83 value={password}
84 onChange={(e) => setPassword(e.target.value)}
85 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"
86 placeholder="Password"
87 />
88 </div>
89 </div>
90
91 <div className="flex items-center justify-between">
92 <div className="flex items-center">
93 <input
94 id="remember-me"
95 name="remember-me"
96 type="checkbox"
97 className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
98 />
99 <label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">
100 Remember me
101 </label>
102 </div>
103
104 <div className="text-sm">
105 <Link href="/forgot-password" className="font-medium text-indigo-600 hover:text-indigo-500">
106 Forgot your password?
107 </Link>
108 </div>
109 </div>
110
111 <div>
112 <button
113 type="submit"
114 disabled={loading}
115 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"
116 >
117 {loading ? 'Signing in...' : 'Sign in'}
118 </button>
119 </div>
120 </form>
121 </div>
122 </div>
123 );
124}
Registration Page
1// app/signup/page.tsx
2"use client";
3
4import { useState } from 'react';
5import { signUp } from '@/lib/client';
6import Link from 'next/link';
7import { useRouter } from 'next/navigation';
8
9export default function SignUp() {
10 const [email, setEmail] = useState('');
11 const [password, setPassword] = useState('');
12 const [name, setName] = useState('');
13 const [error, setError] = useState('');
14 const [loading, setLoading] = useState(false);
15 const router = useRouter();
16
17 const handleSubmit = async (e) => {
18 e.preventDefault();
19 setError('');
20 setLoading(true);
21
22 try {
23 const { data, error } = await signUp.email({
24 email,
25 password,
26 name,
27 });
28
29 if (error) {
30 setError(error.message);
31 } else {
32 router.push('/verify-email-sent');
33 }
34 } catch (err) {
35 setError('An error occurred during sign up');
36 } finally {
37 setLoading(false);
38 }
39 };
40
41 return (
42 <div className="min-h-screen flex items-center justify-center bg-gray-50">
43 <div className="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-md">
44 <div className="text-center">
45 <h2 className="mt-6 text-3xl font-bold text-gray-900">Create an account</h2>
46 <p className="mt-2 text-sm text-gray-600">
47 Or{' '}
48 <Link href="/signin" className="font-medium text-indigo-600 hover:text-indigo-500">
49 sign in to your account
50 </Link>
51 </p>
52 </div>
53
54 <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
55 {error && (
56 <div className="bg-red-50 border-l-4 border-red-500 p-4 text-red-700">
57 <p>{error}</p>
58 </div>
59 )}
60
61 <div className="rounded-md shadow-sm space-y-4">
62 <div>
63 <label htmlFor="name" className="sr-only">Full name</label>
64 <input
65 id="name"
66 name="name"
67 type="text"
68 autoComplete="name"
69 required
70 value={name}
71 onChange={(e) => setName(e.target.value)}
72 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"
73 placeholder="Full name"
74 />
75 </div>
76 <div>
77 <label htmlFor="email" className="sr-only">Email address</label>
78 <input
79 id="email"
80 name="email"
81 type="email"
82 autoComplete="email"
83 required
84 value={email}
85 onChange={(e) => setEmail(e.target.value)}
86 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"
87 placeholder="Email address"
88 />
89 </div>
90 <div>
91 <label htmlFor="password" className="sr-only">Password</label>
92 <input
93 id="password"
94 name="password"
95 type="password"
96 autoComplete="new-password"
97 required
98 value={password}
99 onChange={(e) => setPassword(e.target.value)}
100 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"
101 placeholder="Password"
102 />
103 </div>
104 </div>
105
106 <div>
107 <button
108 type="submit"
109 disabled={loading}
110 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"
111 >
112 {loading ? 'Creating account...' : 'Sign up'}
113 </button>
114 </div>
115 </form>
116 </div>
117 </div>
118 );
119}
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:
1// middleware.ts
2import { rateLimit } from 'express-rate-limit';
3
4// Limit login attempts
5const loginLimiter = rateLimit({
6 windowMs: 15 * 60 * 1000, // 15 minutes
7 max: 5, // 5 maximum attempts
8 message: { error: 'Too many login attempts. Please try again later.' },
9});
10
11// Apply limiter to authentication routes
12app.use('/api/auth/signin', loginLimiter);
Environment Variables
Always store sensitive information in environment variables:
1# .env.local
2DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
3BETTER_AUTH_SECRET="your-very-secure-secret"
4BETTER_AUTH_URL="http://localhost:3000"
5EMAIL_SERVER="smtp://user:pass@smtp.example.com:587"
6EMAIL_FROM="noreply@example.com"
7. Using Sessions in Server Components
Better Auth makes it easy to access user sessions in Server Components:
1// app/profile/page.tsx
2import { auth } from "@/lib/auth";
3import { headers } from "next/headers";
4import { redirect } from "next/navigation";
5
6export default async function ProfilePage() {
7 const session = await auth.api.getSession({
8 headers: headers()
9 });
10
11 if (!session) {
12 redirect('/signin');
13 }
14
15 return (
16 <div className="container mx-auto p-6">
17 <h1 className="text-2xl font-bold mb-4">Profile</h1>
18 <div className="bg-white shadow rounded-lg p-6">
19 <div className="flex items-center space-x-4">
20 {session.user.image && (
21 <img
22 src={session.user.image}
23 alt={session.user.name || 'User'}
24 className="h-16 w-16 rounded-full"
25 />
26 )}
27 <div>
28 <h2 className="text-xl font-medium">{session.user.name}</h2>
29 <p className="text-gray-500">{session.user.email}</p>
30 </div>
31 </div>
32 </div>
33 </div>
34 );
35}
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!