Better Auth is a TypeScript-first authentication library with first-class Next.js and database ORM support — betterAuth({ database, emailAndPassword, socialProviders }) configures the server. createAuthClient() creates a typed client. authClient.signIn.email({ email, password }) logs in; authClient.signUp.email({ email, password, name }) registers. authClient.useSession() returns the current session as a React hook. OAuth is added with socialProvider({ providerId: "google", clientId, clientSecret }). The middleware plugin enforces route protection server-side. organization() plugin adds multi-tenancy with invite flows. Better Auth generates TypeScript types for your entire auth surface — routes, session, user, and plugins. It works with Prisma, Drizzle, and raw SQL adapters. Claude Code generates Better Auth server config, client setup, session hooks, OAuth providers, role-based middleware, and database adapter configuration.
CLAUDE.md for Better Auth
## Better Auth Stack
- Version: better-auth >= 1.1
- Server: betterAuth({ database, emailAndPassword: { enabled: true }, socialProviders: [...] })
- Client: const authClient = createAuthClient({ baseURL: "http://localhost:3000" })
- Sign in: await authClient.signIn.email({ email, password, callbackURL: "/dashboard" })
- Sign up: await authClient.signUp.email({ email, password, name })
- Session: const { data: session } = authClient.useSession() — React hook
- Next.js: export { GET, POST } = auth.handler — api/auth/[...all]/route.ts
- Middleware: auth.api.getSession() in next/server middleware
- Plugins: organization(), twoFactor(), passkey() — modular plugins
Server Configuration
// lib/auth.ts — Better Auth server setup
import { betterAuth } from "better-auth"
import { prismaAdapter } from "better-auth/adapters/prisma"
import { organization, twoFactor } from "better-auth/plugins"
import { db } from "@/lib/db"
export const auth = betterAuth({
database: prismaAdapter(db, {
provider: "postgresql",
}),
// Session configuration
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // Refresh if older than 1 day
cookieCache: {
enabled: true,
maxAge: 5 * 60, // Cache session for 5 minutes client-side
},
},
// Email/password auth
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
minPasswordLength: 8,
autoSignIn: true, // Auto sign in after registration
},
// Email verification
emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
await sendEmail({
to: user.email,
subject: "Verify your email",
html: `<a href="${url}">Verify email</a>`,
})
},
expiresIn: 3_600, // 1 hour
},
// Password reset
emailAndPassword: {
enabled: true,
sendResetPassword: async ({ user, url }) => {
await sendEmail({
to: user.email,
subject: "Reset your password",
html: `<a href="${url}">Reset password</a>`,
})
},
},
// OAuth providers
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
scope: ["openid", "email", "profile"],
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
// Plugins
plugins: [
organization({
allowUserToCreateOrganization: true,
invitationExpiresIn: 48 * 3_600, // 48 hours
sendInvitationEmail: async ({ invitation, inviteLink }) => {
await sendEmail({
to: invitation.email,
subject: "You've been invited",
html: `<a href="${inviteLink}">Accept invitation</a>`,
})
},
}),
twoFactor({
otpOptions: { digits: 6 },
}),
],
// Trusted origins for CORS
trustedOrigins: [
process.env.NEXT_PUBLIC_APP_URL!,
],
// Additional user fields
user: {
additionalFields: {
role: {
type: "string",
defaultValue: "user",
input: false, // Not set by user
},
stripeCustomerId: {
type: "string",
required: false,
input: false,
},
},
},
})
export type Session = typeof auth.$Infer.Session
export type User = typeof auth.$Infer.Session.user
async function sendEmail(_opts: { to: string; subject: string; html: string }) {
// Your email sending implementation
}
Next.js Route Handler
// app/api/auth/[...all]/route.ts — catch-all auth handler
import { auth } from "@/lib/auth"
import { toNextJsHandler } from "better-auth/next-js"
export const { GET, POST } = toNextJsHandler(auth)
Auth Client
// lib/auth-client.ts — browser auth client
import { createAuthClient } from "better-auth/react"
import { organizationClient, twoFactorClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000",
plugins: [
organizationClient(),
twoFactorClient({
twoFactorPage: "/auth/two-factor",
}),
],
})
// Re-export typed hooks and methods
export const {
signIn,
signUp,
signOut,
useSession,
getSession,
} = authClient
React Components
// components/auth/SignInForm.tsx — email/password sign in
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { signIn } from "@/lib/auth-client"
export function SignInForm() {
const router = useRouter()
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setError(null)
setLoading(true)
const formData = new FormData(e.currentTarget)
const { error } = await signIn.email({
email: formData.get("email") as string,
password: formData.get("password") as string,
callbackURL: "/dashboard",
})
if (error) {
setError(error.message ?? "Sign in failed")
setLoading(false)
} else {
router.push("/dashboard")
}
}
async function handleGoogleSignIn() {
await signIn.social({
provider: "google",
callbackURL: "/dashboard",
})
}
return (
<div className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-4">
<input name="email" type="email" required placeholder="Email" className="input" />
<input name="password" type="password" required placeholder="Password" className="input" />
{error && <p className="text-sm text-red-500">{error}</p>}
<button type="submit" disabled={loading} className="btn-primary w-full">
{loading ? "Signing in..." : "Sign in"}
</button>
</form>
<div className="relative">
<hr />
<span className="absolute inset-x-0 -top-2.5 text-center text-xs text-muted-foreground bg-background px-2 mx-auto w-8">or</span>
</div>
<button onClick={handleGoogleSignIn} className="btn-outline w-full">
Continue with Google
</button>
</div>
)
}
// components/auth/SessionProvider.tsx — access session in components
"use client"
import { useSession } from "@/lib/auth-client"
import { redirect } from "next/navigation"
export function RequireAuth({ children }: { children: React.ReactNode }) {
const { data: session, isPending } = useSession()
if (isPending) return <div>Loading...</div>
if (!session) redirect("/auth/sign-in")
return <>{children}</>
}
Server-Side Session + Middleware
// middleware.ts — protect routes
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import { auth } from "@/lib/auth"
const PUBLIC_PATHS = ["/", "/auth/sign-in", "/auth/sign-up", "/api/auth"]
export async function middleware(request: NextRequest) {
const isPublic = PUBLIC_PATHS.some(p => request.nextUrl.pathname.startsWith(p))
if (isPublic) return NextResponse.next()
const session = await auth.api.getSession({
headers: request.headers,
})
if (!session) {
const signInUrl = new URL("/auth/sign-in", request.url)
signInUrl.searchParams.set("callbackURL", request.nextUrl.pathname)
return NextResponse.redirect(signInUrl)
}
// Role-based access
if (request.nextUrl.pathname.startsWith("/admin")) {
if (session.user.role !== "admin") {
return NextResponse.redirect(new URL("/403", request.url))
}
}
return NextResponse.next()
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
}
// app/dashboard/page.tsx — server-side session in Server Component
import { headers } from "next/headers"
import { auth } from "@/lib/auth"
import { redirect } from "next/navigation"
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers(),
})
if (!session) redirect("/auth/sign-in")
return (
<div>
<h1>Welcome, {session.user.name}</h1>
<p className="text-muted-foreground">{session.user.email}</p>
</div>
)
}
For the NextAuth.js (Auth.js) alternative when a more established ecosystem with a larger plugin library (25+ providers) and official framework adapters for SvelteKit, Express, and Fastify is needed — Auth.js v5 has a similar server-first design with auth() for Server Components and middleware, see the authentication comparison guide. For the Lucia alternative when a lower-level auth toolkit with no magic — just session management helpers and database adapters — is preferred to build a completely custom auth flow without any pre-built email/OAuth strategies, see the custom authentication guide. The Claude Skills 360 bundle includes Better Auth skill sets covering email/password, OAuth, organizations, and session middleware. Start with the free tier to try authentication generation.