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>
}

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

External Resources

Documentation

Comments