Introduction
Authentication is critical for every application, but building it from scratch is risky and time-consuming. For startups, using a managed auth solution is the smart choice. This guide compares the top three: Clerk, Auth.js (NextAuth), and Supabase Auth.
Comparison Overview
# Auth solution comparison
solutions:
- name: "Clerk"
type: "Managed Auth Service"
complexity: "Very Low"
providers: "OAuth, Email, Magic Links, Passkeys"
mfa: "Yes"
user_mgmt: "Yes (built-in dashboard)"
pricing: "Generous free tier"
- name: "Auth.js (NextAuth)"
type: "Open-source Library"
complexity: "Medium"
providers: "OAuth, Email, Credentials"
mfa: "Manual implementation"
user_mgmt: "Build yourself"
pricing: "Free (open-source)"
- name: "Supabase Auth"
type: "BaaS Feature"
complexity: "Low"
providers: "OAuth, Email, Magic Links"
mfa: "Limited"
user_mgmt: "Basic (via database)"
pricing: "Free (with Supabase)"
Clerk
Quick Setup
# Install Clerk
npm install @clerk/nextjs
# Wrap your app with ClerkProvider
# app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'
export default function RootLayout({ children }) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
)
}
Sign In Component
// Using Clerk components directly
import { SignIn, SignedIn, SignedOut, UserButton } from '@clerk/nextjs'
export function Header() {
return (
<header>
<SignedOut>
<SignIn />
</SignedOut>
<SignedIn>
<UserButton afterSignOutUrl="/" />
</SignedIn>
</header>
)
}
Custom Sign-In Page
// pages/sign-in/[[...sign-in]].tsx
import { SignIn } from '@clerk/nextjs'
export default function SignInPage() {
return (
<SignIn
appearance={{
elements: {
rootBox: 'mx-auto',
card: 'shadow-lg'
}
}}
routing="path"
path="/sign-in"
signUpUrl="/sign-up"
redirectUrl="/dashboard"
/>
)
}
Protected Routes
// middleware.ts
import { authMiddleware } from '@clerk/nextjs'
export default authMiddleware({
publicRoutes: ['/', '/about', '/api/webhooks(.*)']
})
export const config = {
matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)']
}
// Protect individual routes
import { auth } from '@clerk/nextjs'
export default function DashboardPage() {
const { userId } = auth()
if (!userId) {
redirect('/sign-in')
}
return <div>Welcome to your dashboard!</div>
}
User Management Dashboard
// Clerk provides free user management
// Just use their hosted dashboard at clerk.com
// No code needed!
Auth.js (NextAuth)
Setup
# Install NextAuth
npm install next-auth
# Create auth configuration
mkdir -p app/api/auth/\[...nextauth\]
Configuration
// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth"
import GitHubProvider from "next-auth/providers/github"
import GoogleProvider from "next-auth/providers/google"
import EmailProvider from "next-auth/providers/email"
const handler = NextAuth({
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
}),
],
callbacks: {
async session({ session, token }) {
// Add user ID to session
if (session.user) {
session.user.id = token.sub!
}
return session
},
},
pages: {
signIn: '/auth/signin',
error: '/auth/error',
},
})
export { handler as GET, handler as POST }
Using Auth in Components
// Get current user
import { useSession, signIn, signOut } from 'next-auth/react'
export function AuthButton() {
const { data: session } = useSession()
if (session) {
return (
<button onClick={() => signOut()}>
Sign out
</button>
)
}
return (
<button onClick={() => signIn('github')}>
Sign in
</button>
)
}
Protected Route (Server Component)
// app/dashboard/page.tsx
import { getServerSession } from "next-auth"
import { redirect } from "next/navigation"
export default async function Dashboard() {
const session = await getServerSession()
if (!session) {
redirect("/api/auth/signin")
}
return <h1>Welcome {session.user?.name}</h1>
}
Supabase Auth
Setup
// lib/supabase/auth.ts
import { createClient } from '@supabase/supabase-js'
// Client-side
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// Server-side
import { createServerClient } from '@supabase/ssr'
export async function createClient(request: Request) {
let supabaseResponse = new Response('internal error', { status: 500 })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.headers.get('cookie')?.split('; ') || []
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
request.headers.set('cookie', `${name}=${value}`)
)
supabaseResponse = new Response('ok', { status: 200 })
},
},
}
)
return { supabase, supabaseResponse }
}
Sign Up / Sign In
// Sign up with email
async function signUp(email: string, password: string) {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${location.origin}/auth/callback`,
},
})
}
// Sign in
async function signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
}
// OAuth sign in
async function signInWithGitHub() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${location.origin}/auth/callback`,
},
})
}
Protected Routes
// Server component with auth
import { createClient } from '@/lib/supabase/server'
export default async function Dashboard() {
const { supabase } = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
return <h1>Welcome {user.email}</h1>
}
Middleware Protection
// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => {
request.cookies.set(name, value)
})
supabaseResponse = NextResponse.next({ request })
cookiesToSet.forEach(({ name, value, options }) => {
supabaseResponse.cookies.set(name, value, options)
})
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
if (!user && !request.nextUrl.pathname.startsWith('/login')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return supabaseResponse
}
Pricing Comparison
# Auth pricing (2025)
clerk:
free:
monthly_users: "10,000"
monthly_active_users: "2,500"
features: "All core features"
pro:
price: "$25/month"
maus: "25,000"
features: "SSO, SAML, custom domains"
nextauth:
free:
price: "$0 (open-source)"
# But you need to host it
hosting:
vercel: "$0-20/month"
supabase_auth:
free:
mau: "50,000"
features: "Core auth"
pro:
price: "$25/month"
mau: "100,000"
Decision Guide
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Auth Solution Selection โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Choose CLERK if: โ
โ โข Want fastest time to market โ
โ โข Need built-in user management UI โ
โ โข Don't want to maintain auth infrastructure โ
โ โข Need MFA, SSO, enterprise features โ
โ โ
โ Choose AUTH.JS if: โ
โ โข Want full control over everything โ
โ โข Prefer open-source solutions โ
โ โข Have time to implement user management โ
โ โข Already using Next.js โ
โ โ
โ Choose SUPABASE AUTH if: โ
โ โข Using Supabase for database โ
โ โข Want all-in-one BaaS โ
โ โข Need tight database integration โ
โ โข Prefer PostgreSQL triggers for auth events โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key Takeaways
- Clerk - Easiest, most feature-rich, great for speed
- Auth.js - Full control, free, requires more work
- Supabase Auth - Best paired with Supabase database
- All three - Secure and production-ready
Comments