Best Next.js Authentication Solutions in 2026
·PkgPulse Team
TL;DR
Auth.js (NextAuth v5) for self-hosted OAuth; Clerk for full-featured managed auth; Better Auth for type-safe self-hosted. Auth.js (~2.5M weekly downloads) is the standard Next.js auth library — handles OAuth in minutes but requires database setup for sessions. Clerk (~300K) is managed auth as a service with beautiful pre-built UI — $0 for 10K monthly users. Better Auth (~100K, fast-growing) is a newer TypeScript-first alternative to NextAuth with better type safety and plugin system.
Key Takeaways
- Auth.js v5: ~2.5M weekly downloads — App Router, Server Actions, edge compatible
- Clerk: ~300K downloads — managed, pre-built UI, $0 for 10K users/month
- Better Auth: ~100K downloads — type-safe, plugin architecture, newer alternative
- Lucia v3 — session management library (not full auth, you build on top)
- NextAuth v4 → v5 — major API change; v5 is Auth.js, works across frameworks
Auth.js v5 (Next.js Standard)
// Auth.js v5 — setup
// auth.ts (project root)
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';
import Credentials from 'next-auth/providers/credentials';
import { DrizzleAdapter } from '@auth/drizzle-adapter';
import { db } from '@/db';
import { verifyPassword } from '@/lib/crypto';
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: DrizzleAdapter(db), // Persist sessions to DB
providers: [
GitHub({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
Credentials({
credentials: {
email: { type: 'email' },
password: { type: 'password' },
},
authorize: async ({ email, password }) => {
const user = await db.query.users.findFirst({
where: eq(users.email, email as string),
});
if (!user) return null;
const valid = await verifyPassword(password as string, user.passwordHash);
return valid ? user : null;
},
}),
],
callbacks: {
session({ session, user }) {
session.user.id = user.id;
session.user.role = user.role;
return session;
},
},
pages: {
signIn: '/login',
error: '/auth/error',
},
});
// Auth.js v5 — App Router: route handlers
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';
export const { GET, POST } = handlers;
// Auth.js v5 — middleware (protect routes)
// middleware.ts
import { auth } from '@/auth';
import { NextResponse } from 'next/server';
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isAuthPage = req.nextUrl.pathname.startsWith('/login');
const isProtected = req.nextUrl.pathname.startsWith('/dashboard');
if (isProtected && !isLoggedIn) {
return NextResponse.redirect(new URL('/login', req.url));
}
if (isAuthPage && isLoggedIn) {
return NextResponse.redirect(new URL('/dashboard', req.url));
}
});
export const config = {
matcher: ['/dashboard/:path*', '/login'],
};
// Auth.js v5 — Server Component auth check
// app/dashboard/page.tsx
import { auth } from '@/auth';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const session = await auth();
if (!session) redirect('/login');
return (
<div>
<p>Welcome, {session.user?.name}</p>
<p>Role: {session.user?.role}</p>
</div>
);
}
Clerk (Managed Auth)
// Clerk — setup in Next.js App Router
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher(['/', '/sign-in(.*)', '/sign-up(.*)']);
export default clerkMiddleware((auth, req) => {
if (!isPublicRoute(req)) auth().protect();
});
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};
// Clerk — wrap app
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html>
<body>{children}</body>
</html>
</ClerkProvider>
);
}
// Clerk — pre-built UI components (zero custom UI needed)
import { SignIn, SignUp, UserButton, SignedIn, SignedOut } from '@clerk/nextjs';
// Ready-made sign-in page
export default function SignInPage() {
return <SignIn />;
}
// Conditional rendering
export function Header() {
return (
<header>
<SignedIn>
<UserButton afterSignOutUrl="/" />
</SignedIn>
<SignedOut>
<a href="/sign-in">Sign In</a>
</SignedOut>
</header>
);
}
// Clerk — server-side user access
import { auth, currentUser } from '@clerk/nextjs/server';
export default async function ProfilePage() {
const { userId } = auth();
if (!userId) redirect('/sign-in');
const user = await currentUser();
return (
<div>
<p>{user?.firstName} {user?.lastName}</p>
<p>{user?.emailAddresses[0]?.emailAddress}</p>
</div>
);
}
Better Auth
// Better Auth — type-safe auth setup
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { db } from '@/db';
import { nextCookies } from 'better-auth/next-js';
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: 'pg' }),
emailAndPassword: { enabled: true },
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
plugins: [nextCookies()],
});
export type Session = typeof auth.$Infer.Session;
export type User = typeof auth.$Infer.Session.user;
When to Choose
| Scenario | Pick |
|---|---|
| New Next.js app, want zero auth work | Clerk |
| Budget-conscious, under 10K users | Clerk (free) |
| Self-hosted, data stays yours | Auth.js v5 |
| Better type safety than NextAuth | Better Auth |
| Social OAuth only (GitHub, Google) | Auth.js v5 (simplest) |
| Organization/team management | Clerk |
| Custom auth flows, complex requirements | Auth.js v5 or Better Auth |
| Edge runtime required | Auth.js v5 (edge-compatible) |
Compare auth solution package health on PkgPulse.
See the live comparison
View nextauth vs. clerk on PkgPulse →