Better Auth Next.js Setup Guide
Version Requirements
{
"dependencies": {
"better-auth": "^1.2.0",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.0.0"
}
}
Installation
npm install better-auth
npm install next@latest react@latest react-dom@latest
Step 1: Create Auth Client
// lib/auth.ts
import { betterAuth } from 'better-auth';
import { nextCookies } from 'better-auth/next-js';
export const auth = betterAuth({
baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000',
plugins: [],
});
// lib/auth/client.ts
import { createAuthClient } from 'better-auth/react';
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || 'http://localhost:3000',
});
Step 2: Create Auth API Route
// app/api/auth/[...auth]/route.ts
import { auth } from '@/lib/auth';
import { toNextJsHandler } from 'better-auth/next-js';
export const { GET, POST } = toNextJsHandler(auth);
Step 3: Create Auth Pages
Sign In Page
// app/(auth)/sign-in/page.tsx
'use client';
import { authClient } from '@/lib/auth/client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function SignInPage() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleEmailSignIn = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const { data, error } = await authClient.signIn.email({
email,
password,
});
if (error) {
setError(error.message);
return;
}
router.push('/dashboard');
router.refresh();
} catch (err) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
const handleGitHubSignIn = async () => {
await authClient.signIn.social({
provider: 'github',
callbackURL: '/dashboard',
});
};
const handleGoogleSignIn = async () => {
await authClient.signIn.social({
provider: 'google',
callbackURL: '/dashboard',
});
};
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-8">
<div>
<h2 className="text-3xl font-bold">Sign in to your account</h2>
</div>
{/* Social Sign In */}
<div className="space-y-4">
<button
className="w-full rounded-md border p-3 hover:bg-gray-50"
>
Continue with GitHub
</button>
<button
className="w-full rounded-md border p-3 hover:bg-gray-50"
>
Continue with Google
</button>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white px-2 text-gray-500">Or continue with</span>
</div>
</div>
{/* Email/Password Sign In */}
<form className="space-y-6">
{error && (
<div className="rounded-md bg-red-50 p-4 text-sm text-red-800">
{error}
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email address
</label>
<input
id="email"
type="email"
required
value={email}
=> setEmail(e.target.value)}
className="mt-1 block w-full rounded-md border p-2"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<input
id="password"
type="password"
required
value={password}
=> setPassword(e.target.value)}
className="mt-1 block w-full rounded-md border p-2"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full rounded-md bg-blue-600 p-3 text-white hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Signing in...' : 'Sign in'}
</button>
</form>
<p className="text-center text-sm">
Don't have an account?{' '}
<a href="/sign-up" className="text-blue-600 hover:underline">
Sign up
</a>
</p>
</div>
</div>
);
}
Sign Up Page
// app/(auth)/sign-up/page.tsx
'use client';
import { authClient } from '@/lib/auth/client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function SignUpPage() {
const router = useRouter();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const { data, error } = await authClient.signUp.email({
email,
password,
name,
});
if (error) {
setError(error.message);
return;
}
router.push('/dashboard');
router.refresh();
} catch (err) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-8">
<div>
<h2 className="text-3xl font-bold">Create your account</h2>
</div>
<form className="space-y-6">
{error && (
<div className="rounded-md bg-red-50 p-4 text-sm text-red-800">
{error}
</div>
)}
<div>
<label htmlFor="name" className="block text-sm font-medium">
Full name
</label>
<input
id="name"
type="text"
required
value={name}
=> setName(e.target.value)}
className="mt-1 block w-full rounded-md border p-2"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email address
</label>
<input
id="email"
type="email"
required
value={email}
=> setEmail(e.target.value)}
className="mt-1 block w-full rounded-md border p-2"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<input
id="password"
type="password"
required
minLength={8}
value={password}
=> setPassword(e.target.value)}
className="mt-1 block w-full rounded-md border p-2"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full rounded-md bg-blue-600 p-3 text-white hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Creating account...' : 'Sign up'}
</button>
</form>
<p className="text-center text-sm">
Already have an account?{' '}
<a href="/sign-in" className="text-blue-600 hover:underline">
Sign in
</a>
</p>
</div>
</div>
);
}
Step 4: Protected Page Example
// app/(dashboard)/dashboard/page.tsx
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const session = await auth();
if (!session) {
redirect('/sign-in');
}
return (
<div>
<h1>Welcome, {session.user.name}</h1>
<p>Email: {session.user.email}</p>
<pre>{JSON.stringify(session, null, 2)}</pre>
</div>
);
}
Step 5: Sign Out Component
// components/sign-out-button.tsx
'use client';
import { authClient } from '@/lib/auth/client';
import { useRouter } from 'next/navigation';
export function SignOutButton() {
const router = useRouter();
const handleSignOut = async () => {
await authClient.signOut();
router.push('/sign-in');
router.refresh();
};
return (
<button
Sign out
</button>
);
}
Step 6: Middleware for Route Protection
// middleware.ts
import { auth } from '@/lib/auth';
import { nextMiddleware } from 'better-auth/next-js';
export default nextMiddleware(auth);
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
Or with custom middleware:
// middleware.ts
import { auth } from '@/lib/auth';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
const session = await auth.api.getSession({
headers: request.headers,
});
const isAuthPage = request.nextUrl.pathname.startsWith('/sign-in') ||
request.nextUrl.pathname.startsWith('/sign-up');
const isProtectedRoute = request.nextUrl.pathname.startsWith('/dashboard');
if (isAuthPage && session) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
if (isProtectedRoute && !session) {
return NextResponse.redirect(new URL('/sign-in', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
Step 7: Client-side Session Hook
// hooks/use-session.ts
'use client';
import { useEffect, useState } from 'react';
import type { Session } from 'better-auth/types';
export function useSession() {
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadSession() {
try {
const response = await fetch('/api/auth/get-session');
const data = await response.json();
setSession(data);
} catch {
setSession(null);
} finally {
setLoading(false);
}
}
loadSession();
}, []);
return { session, loading };
}
Step 8: Server Actions for Auth
// app/actions.ts
'use server';
import { auth } from '@/lib/auth';
import { headers } from 'next/headers';
export async function getSession() {
return auth.api.getSession({
headers: await headers(),
});
}
export async function signOut() {
return auth.api.signOut({
headers: await headers(),
});
}
Environment Variables
# .env.local
BETTER_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000
# If using external backend:
# BETTER_AUTH_URL=https://api.example.com
# NEXT_PUBLIC_BETTER_AUTH_URL=https://app.example.com
App Router Structure
app/
├── (auth)/
│ ├── sign-in/
│ │ └── page.tsx
│ └── sign-up/
│ └── page.tsx
├── (dashboard)/
│ ├── dashboard/
│ │ └── page.tsx
│ └── layout.tsx
├── api/
│ └── auth/
│ └── [...auth]/
│ └── route.ts
├── layout.tsx
└── page.tsx