Better Auth Setup#
Complete authentication setup for Next.js with Better Auth - email/password login, signup, and session management.
Installation#
npm install better-auth
Database Setup#
Better Auth needs a database to store users and sessions. We'll use PostgreSQL with Drizzle ORM.
npm install drizzle-orm postgres
npm install -D drizzle-kit
Environment Variables#
# .env.local
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
BETTER_AUTH_SECRET="your-super-secret-key-change-this"
BETTER_AUTH_URL="http://localhost:3000"
Auth Configuration#
// lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./db";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
}),
emailAndPassword: {
enabled: true,
minPasswordLength: 8,
},
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // Update every 24 hours
},
advanced: {
cookiePrefix: "my-app",
},
});
Database Connection#
// lib/db.ts
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
const connectionString = process.env.DATABASE_URL!;
const client = postgres(connectionString);
export const db = drizzle(client);
API Route Handler#
// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);
This creates all auth endpoints:
- POST /api/auth/sign-up/email - Signup
- POST /api/auth/sign-in/email - Login
- POST /api/auth/sign-out - Logout
- GET /api/auth/get-session - Get current session
Client-Side Auth Hook#
// 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",
});
export const {
signIn,
signUp,
signOut,
useSession,
} = authClient;
Run Database Migrations#
Better Auth uses its own schema. Generate and run migrations:
# Generate schema
npx better-auth migrate
# Or manually create tables
npx drizzle-kit push
Better Auth will create these tables:
- user - User accounts
- session - Active sessions
- account - OAuth accounts (if using OAuth)
- verification - Email verification tokens
Usage in Components#
Get Session (Server Component)#
// app/dashboard/page.tsx
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
redirect("/login");
}
return (
<div>
<h1>Welcome, {session.user.name}!</h1>
<p>Email: {session.user.email}</p>
</div>
);
}
Get Session (Client Component)#
'use client'
import { useSession } from "@/lib/auth-client";
export default function ProfileButton() {
const { data: session, isPending } = useSession();
if (isPending) {
return <div>Loading...</div>;
}
if (!session) {
return <a href="/login">Login</a>;
}
return (
<div>
<p>Logged in as {session.user.email}</p>
</div>
);
}
Complete Auth Flow#
1. Signup#
'use client'
import { signUp } from "@/lib/auth-client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function SignupForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const router = useRouter();
const handleSignup = async (e: React.FormEvent) => {
e.preventDefault();
const { data, error } = await signUp.email({
email,
password,
name,
});
if (error) {
alert(error.message);
return;
}
// Redirect to dashboard
router.push("/dashboard");
};
return (
<form onSubmit={handleSignup}>
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">Sign Up</button>
</form>
);
}
2. Login#
'use client'
import { signIn } from "@/lib/auth-client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const router = useRouter();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
const { data, error } = await signIn.email({
email,
password,
});
if (error) {
alert(error.message);
return;
}
router.push("/dashboard");
};
return (
<form onSubmit={handleLogin}>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">Login</button>
</form>
);
}
3. Logout#
'use client'
import { signOut } from "@/lib/auth-client";
import { useRouter } from "next/navigation";
export default function LogoutButton() {
const router = useRouter();
const handleLogout = async () => {
await signOut();
router.push("/login");
};
return (
<button onClick={handleLogout}>
Logout
</button>
);
}
Protected Routes (Server Side)#
// app/(protected)/layout.tsx
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function ProtectedLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
redirect("/login");
}
return <>{children}</>;
}
Now all pages in app/(protected)/ require authentication!
Middleware Protection (Alternative)#
// 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,
});
// Protect /dashboard routes
if (request.nextUrl.pathname.startsWith("/dashboard")) {
if (!session) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
// Redirect logged-in users away from auth pages
if (request.nextUrl.pathname.startsWith("/login") ||
request.nextUrl.pathname.startsWith("/signup")) {
if (session) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/login", "/signup"],
};
TypeScript Types#
// types/auth.ts
export interface User {
id: string;
email: string;
name: string;
emailVerified: boolean;
image?: string;
createdAt: Date;
updatedAt: Date;
}
export interface Session {
session: {
userId: string;
expiresAt: Date;
};
user: User;
}
Environment Variables Reference#
# Required
DATABASE_URL="postgresql://..."
BETTER_AUTH_SECRET="random-secret-key"
# Optional
BETTER_AUTH_URL="http://localhost:3000" # For production
NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000"
Best Practices#
- ✅ Use server-side session checks for protected pages
- ✅ Validate on server - Never trust client-side checks only
- ✅ Use middleware for route-level protection
- ✅ Handle errors gracefully - Show user-friendly messages
- ✅ Set strong secrets - Use long random strings
- ✅ Session expiry - Configure appropriate session length
Common Patterns#
Check Auth in Server Action#
// app/actions.ts
'use server'
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
export async function updateProfile(name: string) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
throw new Error("Unauthorized");
}
// Update user profile
await db.update(users)
.set({ name })
.where(eq(users.id, session.user.id));
return { success: true };
}
Conditional UI#
'use client'
import { useSession } from "@/lib/auth-client";
export default function Navbar() {
const { data: session } = useSession();
return (
<nav>
{session ? (
<>
<a href="/dashboard">Dashboard</a>
<a href="/profile">Profile</a>
</>
) : (
<>
<a href="/login">Login</a>
<a href="/signup">Sign Up</a>
</>
)}
</nav>
);
}
Next Steps#
- Auth UI Components - Build login and signup pages
- Getting Started - Next.js project setup