Clerk vs NextAuth v5: Modern Authentication in Next.js 2026
TL;DR
Clerk if you want auth done in 30 minutes and don't mind paying. NextAuth v5 if you want control and the $0 cost at any scale. Clerk provides a full auth UI, user management dashboard, organizations, MFA, and device sessions out of the box — but at $0.02/MAU after 10K users, it gets expensive. NextAuth v5 (now called Auth.js) handles the hard parts of OAuth but requires you to build the user management UI. The break-even where Clerk becomes expensive: around 5,000-10,000 monthly active users.
Key Takeaways
- Clerk: hosted auth + UI components, $0 free to 10K MAU then $0.02/MAU, fastest setup
- NextAuth v5 (Auth.js): self-hosted, free, requires Postgres adapter, requires custom UI
- Clerk advantages: Organizations, device sessions, MFA, user impersonation, JWT templates
- NextAuth advantages: Full control, zero vendor lock-in, free at any scale, self-hostable
- Break-even: Clerk becomes expensive vs NextAuth at ~5,000 active users ($100/month)
- Migration difficulty: switching auth providers is painful (passwords can't be migrated)
Clerk: Full-Service Auth
Best for: teams that want auth to "just work", B2B SaaS needing organizations, quick prototypes
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
npm install @clerk/nextjs
// app/layout.tsx — wrap your app:
import { ClerkProvider } from '@clerk/nextjs';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}
// middleware.ts — protect routes:
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/pricing',
'/api/webhooks(.*)', // Webhooks are public
]);
export default clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect(); // Redirects to sign-in if not authenticated
}
});
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};
// app/dashboard/page.tsx — server component with user:
import { auth, currentUser } from '@clerk/nextjs/server';
export default async function Dashboard() {
const { userId } = await auth();
const user = await currentUser();
return (
<div>
<h1>Hello, {user?.firstName}!</h1>
<p>User ID: {userId}</p>
</div>
);
}
// Client component:
'use client';
import { useUser, useClerk, SignOutButton } from '@clerk/nextjs';
export function UserMenu() {
const { user } = useUser();
const { openUserProfile } = useClerk();
return (
<div>
<img src={user?.imageUrl} alt={user?.fullName ?? 'User'} />
<button onClick={() => openUserProfile()}>Profile</button>
<SignOutButton>Sign Out</SignOutButton>
</div>
);
}
Clerk Organizations (B2B Multi-Tenancy)
// Built-in org support — no extra code needed:
import { auth } from '@clerk/nextjs/server';
export default async function OrgDashboard() {
const { orgId, orgRole, userId } = await auth();
if (!orgId) redirect('/select-org');
// Org-scoped queries:
const projects = await db.project.findMany({
where: { organizationId: orgId }, // Store orgId in your DB
});
}
Clerk Pricing
Free: 10,000 MAU
Pro: $0.02/MAU after free tier
+ $25/month base
5,000 MAU: $25/month (all in free tier)
10,000 MAU: $25/month (all in free tier)
20,000 MAU: $25 + (10,000 × $0.02) = $225/month
50,000 MAU: $25 + (40,000 × $0.02) = $825/month
For organizations (B2B):
$25/month for Clerk Teams (unlocks org features)
NextAuth v5 (Auth.js): Self-Hosted
Best for: cost at scale, full control, teams comfortable with configuration
npm install next-auth@beta @auth/prisma-adapter
// auth.ts — configure providers:
import NextAuth from 'next-auth';
import { PrismaAdapter } from '@auth/prisma-adapter';
import GitHub from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';
import Credentials from 'next-auth/providers/credentials';
import bcrypt from 'bcryptjs';
import { db } from '@/lib/db';
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(db),
providers: [
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
Credentials({
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
const user = await db.user.findUnique({
where: { email: credentials.email as string },
});
if (!user?.passwordHash) return null;
const valid = await bcrypt.compare(
credentials.password as string,
user.passwordHash
);
return valid ? user : null;
},
}),
],
session: { strategy: 'jwt' }, // or 'database' for server-side sessions
pages: {
signIn: '/auth/sign-in',
error: '/auth/error',
},
callbacks: {
jwt: async ({ token, user }) => {
if (user) {
token.userId = user.id;
token.plan = (user as any).plan ?? 'free';
}
return token;
},
session: async ({ session, token }) => {
session.user.id = token.userId as string;
session.user.plan = token.plan as string;
return session;
},
},
});
// app/api/auth/[...nextauth]/route.ts:
import { handlers } from '@/auth';
export const { GET, POST } = handlers;
// middleware.ts — protect routes:
import { auth } from '@/auth';
import { NextResponse } from 'next/server';
export default auth((req) => {
const isAuthenticated = !!req.auth;
const isAuthPage = req.nextUrl.pathname.startsWith('/auth');
const isPublicPage = ['/', '/pricing', '/about'].includes(req.nextUrl.pathname);
if (!isAuthenticated && !isAuthPage && !isPublicPage) {
const signInUrl = new URL('/auth/sign-in', req.url);
signInUrl.searchParams.set('callbackUrl', req.nextUrl.pathname);
return NextResponse.redirect(signInUrl);
}
return NextResponse.next();
});
export const config = { matcher: ['/((?!api/auth|_next/static|_next/image|.*\\.png$).*)'] };
Prisma Schema (Required for Database Sessions)
// prisma/schema.prisma — NextAuth requires these tables:
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
image String?
passwordHash String? // For credentials auth
plan String @default("free")
accounts Account[]
sessions Session[]
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
Build Your Own UI
NextAuth requires custom UI (no pre-built components):
// app/auth/sign-in/page.tsx:
'use client';
import { signIn } from 'next-auth/react';
import { useState } from 'react';
export default function SignInPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-96 space-y-4">
<h1 className="text-2xl font-bold">Sign In</h1>
{/* Social providers: */}
<button
onClick={() => signIn('github', { callbackUrl: '/dashboard' })}
className="w-full border p-2"
>
Continue with GitHub
</button>
<button
onClick={() => signIn('google', { callbackUrl: '/dashboard' })}
className="w-full border p-2"
>
Continue with Google
</button>
<hr />
{/* Email/password: */}
<form
onSubmit={async (e) => {
e.preventDefault();
await signIn('credentials', { email, password, callbackUrl: '/dashboard' });
}}
>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type="submit">Sign In</button>
</form>
</div>
</div>
);
}
Side-by-Side Comparison
| Clerk | NextAuth v5 | |
|---|---|---|
| Setup time | 30 minutes | 2-3 hours |
| Pre-built UI | ✅ Full UI components | ❌ Build yourself |
| User management dashboard | ✅ | ❌ Build yourself |
| Organizations/Teams | ✅ Built-in | ❌ Build yourself |
| MFA | ✅ Built-in | ❌ Custom implementation |
| Device sessions | ✅ | ❌ |
| Vendor lock-in | High | None |
| Self-hostable | ❌ | ✅ |
| Price at 10K users | Free | Free |
| Price at 50K users | ~$825/month | $0 (infra only) |
| TypeScript support | Excellent | Good |
| OAuth providers | 30+ | 50+ |
When to Choose Each
Choose CLERK if:
→ Building a B2B SaaS with organizations
→ Speed matters more than cost control
→ You need MFA, device sessions, user impersonation without building it
→ Budget is not a concern at current scale
→ You want a slick user management UI for free
Choose NEXTAUTH v5 if:
→ Expected to grow to 20K+ users ($400+/month in Clerk)
→ Compliance requires you to control auth infrastructure
→ Building on-premise / self-hosted
→ Your team is comfortable with more initial setup
→ You want zero vendor lock-in (switch providers easily)
Compare authentication APIs and providers at APIScout.