Auth.js vs Better Auth: which auth library for Next.js in 2026?
Auth.js (the NextAuth lineage) and Better Auth both add sessions, OAuth, and credentials to Next.js App Router apps—but they feel very different on setup time, TypeScript DX, orgs, and MFA. Pick wrong and you pay in bugs, security gaps, or a painful rewrite.
This guide compares Auth.js and Better Auth for real SaaS work: what each does well, where each fights you, and when migration is worth it.
TL;DR
| Feature | Auth.js | BetterAuth |
|---|
| Setup Time | ⏱️ Medium | ⚡ Fast |
| Documentation | 📖 Complex but thorough | 🧼 Clear & minimal |
| Flexibility | 🧩 Very high | 🔒 Opinionated |
| MFA Support | 🔌 Optional | ✅ Built-in |
| Session Strategy | 🔄 JWT/Database | 🔐 JWT only |
| Suitable for Teams? | ✅ Yes | 🤔 Limited |
| TypeScript DX | 😵💫 Inconsistent | 🧠 Strong inference |
When to Use Each
✅ Use Auth.js if:
- You need maximum flexibility (e.g. custom flows, adapters)
- You’re using multiple OAuth providers
- You’re comfortable digging into internals
✅ Use BetterAuth if:
- You want a fast and secure default config
- You prefer simple patterns over flexibility
- You want MFA out of the box
Setup Experience
🔧 Auth.js
Auth.js requires defining multiple layers of config. Here’s a basic setup:
ts
// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
const handler = NextAuth({
providers: [GitHub],
callbacks: {
session: ({ session, token }) => {
session.user.id = token.sub;
return session;
},
},
});
export { handler as GET, handler as POST };
Expect to write more boilerplate for TypeScript types, server/client sync, and session management.
⚡ BetterAuth
BetterAuth ships with sensible defaults and a simplified API:
ts
// app/api/auth/route.ts
import { authHandler } from "betterauth/server";
import GitHub from "betterauth/providers/github";
export const { GET, POST } = authHandler({
providers: [GitHub()],
features: { mfa: true },
});
Cleaner syntax, no extra adapter config, and MFA support enabled by default.
Developer Experience
Auth.js has a steeper learning curve. Docs are comprehensive, but overwhelming. You’ll often end up in Discord or GitHub issues.
BetterAuth’s DX is beginner-friendly. It uses generics and zod under the hood, so type safety “just works.”
💡 Pro tip: BetterAuth auto-inflects types from your session and provider setup — no need for a next-auth.d.ts dance.
Implementation Comparison
Let's look at how both solutions handle common authentication scenarios.
Setting Up Auth.js
First, install the necessary packages:
bash
npm install next-auth
# or
yarn add next-auth
Create an API route for authentication:
ts
// pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
export default NextAuth({
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
// Add your authentication logic here
const user = await authenticateUser(credentials)
if (user) {
return user
}
return null
}
})
],
session: {
strategy: "jwt"
},
callbacks: {
async session({ session, token }) {
// Add custom session handling
return session
}
}
})
Set up the provider in your app:
tsx
// pages/_app.tsx
import { SessionProvider } from "next-auth/react"
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
export default MyApp
Setting Up BetterAuth
Install BetterAuth:
bash
npm install better-auth
# or
yarn add better-auth
Create your auth configuration:
ts
// lib/auth.ts
import { createAuth } from "better-auth"
export const auth = createAuth({
secret: process.env.AUTH_SECRET,
database: {
type: "postgres",
url: process.env.DATABASE_URL
},
session: {
strategy: "jwt"
}
})
Set up the API route:
ts
// pages/api/auth/[...auth].ts
import { auth } from "@/lib/auth"
import { createHandler } from "better-auth/next"
export default createHandler(auth)
Real-World Implementation Examples
Let's explore some common authentication scenarios and how they're handled in both libraries.
User Login with Auth.js
tsx
// components/LoginForm.tsx
import { signIn } from "next-auth/react"
import { useState } from "react"
export default function LoginForm() {
const [credentials, setCredentials] = useState({
email: "",
password: ""
})
const handleSubmit = async (e) => {
e.preventDefault()
try {
const result = await signIn("credentials", {
redirect: false,
email: credentials.email,
password: credentials.password
})
if (result?.error) {
// Handle error
console.error(result.error)
}
} catch (error) {
console.error("Login failed:", error)
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={credentials.email}
onChange={(e) => setCredentials({
...credentials,
email: e.target.value
})}
/>
<input
type="password"
value={credentials.password}
onChange={(e) => setCredentials({
...credentials,
password: e.target.value
})}
/>
<button type="submit">Login</button>
</form>
)
}
User Login with BetterAuth
tsx
// components/LoginForm.tsx
import { useAuth } from "better-auth/react"
import { useState } from "react"
export default function LoginForm() {
const { login } = useAuth()
const [credentials, setCredentials] = useState({
email: "",
password: ""
})
const handleSubmit = async (e) => {
e.preventDefault()
try {
await login({
email: credentials.email,
password: credentials.password
})
// Successful login will automatically update auth state
} catch (error) {
console.error("Login failed:", error)
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={credentials.email}
onChange={(e) => setCredentials({
...credentials,
email: e.target.value
})}
/>
<input
type="password"
value={credentials.password}
onChange={(e) => setCredentials({
...credentials,
password: e.target.value
})}
/>
<button type="submit">Login</button>
</form>
)
}
Protected Routes and Session Management
Auth.js Protected Routes
ts
// middleware.ts
export { default } from "next-auth/middleware"
export const config = {
matcher: ["/protected/:path*"]
}
tsx
// pages/protected/dashboard.tsx
import { useSession } from "next-auth/react"
import { useRouter } from "next/router"
export default function Dashboard() {
const { data: session, status } = useSession()
const router = useRouter()
if (status === "loading") {
return <div>Loading...</div>
}
if (!session) {
router.push("/login")
return null
}
return (
<div>
<h1>Welcome {session.user.name}</h1>
{/* Dashboard content */}
</div>
)
}
BetterAuth Protected Routes
ts
// middleware.ts
import { createMiddleware } from "better-auth/next"
import { auth } from "@/lib/auth"
export default createMiddleware(auth)
export const config = {
matcher: ["/protected/:path*"]
}
tsx
// pages/protected/dashboard.tsx
import { useAuth } from "better-auth/react"
export default function Dashboard() {
const { user, isLoading } = useAuth()
if (isLoading) {
return <div>Loading...</div>
}
return (
<div>
<h1>Welcome {user.name}</h1>
{/* Dashboard content */}
</div>
)
}
Performance and Security Considerations
Auth.js Security Features
JWT Handling:
ts
// pages/api/auth/[...nextauth].ts
export default NextAuth({
jwt: {
secret: process.env.JWT_SECRET,
maxAge: 60 * 60 * 24 * 30, // 30 days
encryption: true
},
security: {
csrf: true,
cookieSecure: process.env.NODE_ENV === "production"
}
})
Custom Authorization Logic:
ts
// lib/auth-checks.ts
export const checkUserPermissions = async (session) => {
if (!session) return false
// Add your custom authorization logic
const userRoles = await fetchUserRoles(session.user.id)
return userRoles.includes("admin")
}
BetterAuth Security Features
Built-in Rate Limiting:
ts
// lib/auth.ts
import { createAuth } from "better-auth"
export const auth = createAuth({
rateLimit: {
enabled: true,
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
},
security: {
passwordPolicy: {
minLength: 8,
requireNumbers: true,
requireSpecialChars: true
}
}
})
MFA Implementation:
ts
// pages/api/auth/enable-mfa.ts
import { auth } from "@/lib/auth"
export default async function handler(req, res) {
const { user } = await auth.getUserSession(req)
if (!user) {
return res.status(401).json({ error: "Unauthorized" })
}
const secret = await auth.mfa.generateSecret()
const qrCode = await auth.mfa.generateQRCode(secret)
return res.json({ secret, qrCode })
}
Migration Considerations
If you're considering migrating from Auth.js to BetterAuth, here's a step-by-step approach:
- Backup Your Data
bash
# Export your users and sessions
pg_dump -t users -t sessions > auth_backup.sql
- Update Dependencies
bash
npm remove next-auth
npm install better-auth
- Update Configuration
ts
// Before (Auth.js)
export default NextAuth({
providers: [...],
callbacks: {...},
session: {...}
})
// After (BetterAuth)
export const auth = createAuth({
providers: {...},
session: {...},
callbacks: {...}
})
- Update Components
ts
// Before (Auth.js)
import { useSession } from "next-auth/react"
// After (BetterAuth)
import { useAuth } from "better-auth/react"
Real-World Migration Example
Let's say you're moving from Auth.js to BetterAuth.
- Replace API route: Change the handler in
app/api/auth/[...nextauth] to app/api/auth/route.ts
- Update client hooks: Replace
useSession from next-auth/react with useBetterSession()
- Remove adapters & JWT callbacks — BetterAuth handles this internally.
- Add MFA (optional) via config flag
ts
// hooks/useSession.ts
import { useSession } from "betterauth/client";
export function useUser() {
const { data } = useSession();
return data?.user;
}
Migration usually takes 1–2 hours for a typical project.
FAQ
Is Better Auth production-ready for Next.js SaaS?
Yes for many App Router SaaS apps: sessions, OAuth, credentials, and org-style features ship with a strong TypeScript DX. Still validate your threat model, email flows, and session storage before a large production cutover.
Should I migrate from Auth.js to Better Auth?
Migrate if TypeScript inference, MFA defaults, or simpler setup are blocking you—and you can schedule a deliberate cutover. Stay on Auth.js if you already depend on its adapter ecosystem, custom callbacks, or a large stable auth surface.
Does Auth.js support multi-factor authentication out of the box?
Not as a first-class built-in the way Better Auth markets MFA. Auth.js can support MFA-style flows via custom logic or community patterns, but expect more glue code.
Which is better for Next.js App Router: Auth.js or Better Auth?
Neither is universally better. Prefer Better Auth for faster defaults and TS-first DX; prefer Auth.js when you need maximum flexibility, mature adapters, or you already run it in production without pain.
Verdict
Go with Better Auth if you want speed, simplicity, and strong security defaults out of the box.
Stick with Auth.js if you need advanced customizations or already have a large app using it.
No matter your choice, both tools can serve you well in 2026 — it depends on your project's complexity and your team's preferences.
Related resources
Got questions or want a migration walkthrough? Contact us.