Better Auth — Complete Implementation with Next.js and Prisma

npmixnpmix
119
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
# 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
# .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
// 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:

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

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

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

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

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

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

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

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

typescript
// 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
# .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:

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

🔗 Prisma adapter guide

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

Similar articles

Never miss an update

Subscribe to receive news and special offers.

By subscribing you agree to our Privacy Policy.