Skip to main content

Clerk vs NextAuth v5: Modern Authentication in Next.js 2026

·APIScout Team
clerknextauthauth-jsauthenticationnext-jsjwt2026

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

ClerkNextAuth v5
Setup time30 minutes2-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-inHighNone
Self-hostable
Price at 10K usersFreeFree
Price at 50K users~$825/month$0 (infra only)
TypeScript supportExcellentGood
OAuth providers30+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.

Comments