Better Auth — Complete Implementation with Next.js and Prisma

Andry Dina
Authentication security in modern web applications

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

bash
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:

env
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:

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:

bash
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:

typescript
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:

typescript
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 registration
  • POST /api/auth/signin - Existing user login
  • POST /api/auth/signout - User logout
  • POST /api/auth/verify-email - Email address verification
  • POST /api/auth/reset-password - Password reset request
  • POST /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:

typescript
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:

typescript
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:

typescript
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:

typescript
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

tsx
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

tsx
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:

typescript
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:

env
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:

tsx
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

🔗 Prisma adapter guide

Feel free to join the Better Auth community to ask questions and share your experiences!

Join our newsletter for the
latest update

By subscribing you agree to receive the Paddle newsletter. Unsubscribe at any time.