Skip to main content
โšก Calmops

Authentication for Startups: Clerk vs Auth.js (NextAuth) vs Supabase Auth

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

External Resources

Comments