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>
}
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 }) {
if (session.user) {
session.user.id = token.sub!
}
return session
},
},
pages: {
signIn: '/auth/signin',
error: '/auth/error',
},
})
export { handler as GET, handler as POST }
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'
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
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,
})
}
Pricing Comparison
# Auth pricing (2025)
clerk:
free:
monthly_users: "10,000"
monthly_active_users: "2,500"
pro:
price: "$25/month"
nextauth:
free:
price: "$0 (open-source)"
hosting:
vercel: "$0-20/month"
supabase_auth:
free:
mau: "50,000"
pro:
price: "$25/month"
Decision Guide
- Clerk - Best for fastest time to market, built-in UI
- Auth.js - Full control, free, requires more work
- Supabase Auth - Best paired with Supabase database
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
Comments