Better Auth vs Clerk vs NextAuth: 2026 SaaS Showdown
Better Auth vs Clerk vs NextAuth: 2026 SaaS Authentication Showdown
Authentication was a solved problem — until it wasn't. NextAuth (now Auth.js) dominated the Next.js auth landscape for years. Then Clerk raised the UX bar with polished pre-built components. Now Better Auth has emerged as a third option: self-hosted like NextAuth, feature-complete like Clerk, and MIT-licensed. This showdown compares all three with actual code examples, current 2026 pricing, and specific recommendations for SaaS builders.
TL;DR
Clerk is the fastest path to production auth with the best UI components and Next.js integration — worth the cost if you have budget and don't want to think about auth. Better Auth is the best free option in 2026 — more complete than NextAuth, no vendor lock-in, built-in passkeys/2FA/organizations. NextAuth v5 (Auth.js) is still the right choice if you need the largest ecosystem and don't mind missing built-in 2FA and RBAC. All three work on Vercel's Edge runtime in 2026.
Key Takeaways
- Better Auth: MIT, self-hosted, built-in 2FA + passkeys + organizations + RBAC — no database vendor required
- Clerk: $0.02/MAU after 10,000 free MAUs — best pre-built components, fastest setup (~5 minutes)
- NextAuth v5: MIT, self-hosted, largest ecosystem — but still missing built-in 2FA and passkeys
- Break-even: Clerk costs ~$100/month at 5,000 MAUs — Better Auth makes sense above that
- Organizations: Better Auth and Clerk both have built-in org/team management; NextAuth requires manual implementation
- Boilerplate adoption: T3 defaults to NextAuth; Supastarter supports Clerk; MakerKit uses Supabase Auth; Better Auth gaining fast in 2025–2026
The State of Next.js Auth in 2026
The auth landscape shifted significantly in 2025. Key events:
- Auth.js v5 released: NextAuth rebranded to Auth.js, added Edge runtime support, improved server action integration
- Better Auth v1.0: First stable release brought plugin architecture for OAuth, 2FA, passkeys, and organizations
- Clerk Organizations GA: Clerk's multi-tenancy features reached general availability, making it a viable Supastarter alternative
- Passkeys adoption: WebAuthn passkeys became mainstream — all three libraries now support them
The practical impact: for new SaaS projects in 2026, the choice is no longer "NextAuth or Clerk." Better Auth is a genuine contender.
Better Auth: Setup and Code
Better Auth is framework-agnostic and integrates with any database via Drizzle or Prisma adapters.
Installation
npm install better-auth
Server Configuration
// lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { twoFactor } from "better-auth/plugins";
import { passkey } from "better-auth/plugins";
import { organization } from "better-auth/plugins";
import { db } from "./db";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
plugins: [
twoFactor(),
passkey(),
organization({
allowUserToCreateOrganization: true,
organizationLimit: 5,
creatorRole: "owner",
membershipLimit: 100,
}),
],
});
API Route Handler
// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
Client Usage
// Client component
"use client";
import { authClient } from "@/lib/auth-client";
export function SignInButton() {
const { data: session } = authClient.useSession();
const signIn = async () => {
await authClient.signIn.social({ provider: "google" });
};
if (session) {
return <button onClick={() => authClient.signOut()}>Sign out</button>;
}
return <button onClick={signIn}>Sign in with Google</button>;
}
Organization Management
// Create and switch organizations
const { data: org } = await authClient.organization.create({
name: "Acme Corp",
slug: "acme",
});
// Invite member with role
await authClient.organization.inviteMember({
email: "user@acme.com",
role: "member",
organizationId: org.id,
});
Schema generation: Better Auth generates database migrations automatically:
npx better-auth generate
# ✓ Generated 8 tables: user, session, account, verification,
# twoFactor, passkey, organization, member
Clerk: Setup and Code
Clerk is a managed service — no database schemas to manage, no auth logic to maintain.
Installation
npm install @clerk/nextjs
Middleware (Required)
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isPublicRoute = createRouteMatcher(["/", "/sign-in(.*)", "/sign-up(.*)"]);
export default clerkMiddleware((auth, request) => {
if (!isPublicRoute(request)) {
auth().protect();
}
});
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
Layout Integration
// app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}
Pre-built UI Components
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from "@clerk/nextjs";
export default function SignInPage() {
return (
<div className="flex items-center justify-center min-h-screen">
<SignIn />
</div>
);
}
That's it. Clerk's <SignIn /> component includes email/password, social OAuth, magic links, passkeys, MFA, and CAPTCHA. No custom UI needed.
Server-Side Auth
// app/dashboard/page.tsx
import { auth, currentUser } from "@clerk/nextjs/server";
export default async function Dashboard() {
const { userId } = auth();
const user = await currentUser();
if (!userId) redirect("/sign-in");
return <div>Welcome, {user?.firstName}</div>;
}
Organizations
// Create org in the Clerk dashboard or via API
import { useOrganization, useOrganizationList } from "@clerk/nextjs";
function OrgSwitcher() {
const { organization } = useOrganization();
const { userMemberships } = useOrganizationList({
userMemberships: { infinite: true },
});
return (
<select>
{userMemberships.data?.map((mem) => (
<option key={mem.organization.id} value={mem.organization.id}>
{mem.organization.name}
</option>
))}
</select>
);
}
NextAuth v5 (Auth.js): Setup and Code
Installation
npm install next-auth@beta
Configuration
// auth.ts
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "@/db";
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: DrizzleAdapter(db),
providers: [
Google,
GitHub,
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
authorize: async (credentials) => {
// Your credential validation logic
const user = await validateCredentials(credentials);
return user ?? null;
},
}),
],
callbacks: {
session({ session, token }) {
session.user.id = token.sub!;
return session;
},
},
});
Route Handler
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
Protected Route
// app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function Dashboard() {
const session = await auth();
if (!session?.user) redirect("/api/auth/signin");
return <div>Welcome, {session.user.name}</div>;
}
What's missing in NextAuth v5: There's no built-in 2FA, no passkeys, no organizations, no RBAC. You implement all of these manually.
Feature Comparison: 2026
| Feature | Better Auth | Clerk | NextAuth v5 |
|---|---|---|---|
| License | MIT | Proprietary (SaaS) | MIT |
| Hosting | Self-hosted | Managed | Self-hosted |
| Email/password | ✓ | ✓ | ✓ |
| OAuth providers | ✓ (40+) | ✓ (20+) | ✓ (50+) |
| Magic links | ✓ | ✓ | ✓ |
| Passkeys | ✓ (plugin) | ✓ | ✗ |
| 2FA / TOTP | ✓ (plugin) | ✓ | ✗ |
| Organizations | ✓ (plugin) | ✓ | ✗ |
| RBAC | ✓ (plugin) | ✓ | ✗ |
| User impersonation | ✓ | ✓ (Enterprise) | ✗ |
| Session management | ✓ | ✓ | ✓ |
| Edge runtime | ✓ | ✓ | ✓ (v5) |
| Pre-built UI | ✗ | ✓ | ✗ |
| Admin dashboard | ✗ | ✓ | ✗ |
| TypeScript | Excellent | Excellent | Good |
| Database required | Yes (own) | No | Yes (own) |
| Setup time | ~30 min | ~5 min | ~45 min |
| MAU pricing | Free (infra cost) | $0.02/MAU >10K | Free (infra cost) |
Pricing Deep Dive
Clerk 2026 Pricing
| Tier | Monthly | MAU Included | Per MAU Over |
|---|---|---|---|
| Free | $0 | 10,000 | N/A (blocked) |
| Pro | $25 | 10,000 | $0.02/MAU |
| Enterprise | Custom | Custom | Custom |
Clerk's Organizations feature requires the Pro plan. At scale:
- 5,000 MAUs → $0 (free tier)
- 15,000 MAUs → $25 + (5,000 × $0.02) = $125/month
- 50,000 MAUs → $25 + (40,000 × $0.02) = $825/month
For SaaS with 50K active users, $825/month is reasonable. For high-growth consumer apps hitting 500K MAUs, the math changes fast.
Better Auth Costs
Better Auth itself is free. Your costs:
- Database: Your existing Postgres (Supabase, Neon, PlanetScale) — already paid
- Email: Resend ($20/month for 50K emails), Postmark, or SendGrid for verification emails
- Infrastructure: No additional compute required beyond your Next.js app
Total additional cost: ~$20–50/month regardless of user count.
NextAuth v5 Costs
Same as Better Auth: free library, pay for your own database and email provider. The difference is that auth complexity (2FA, passkeys) requires building or third-party libraries on top.
Edge Runtime Support in 2026
All three auth libraries now support Vercel Edge runtime and Cloudflare Workers.
Better Auth on Edge:
// app/api/auth/[...all]/route.ts
export const runtime = "edge";
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth.handler);
Clerk on Edge: Works by default — Clerk's middleware already runs on Edge.
NextAuth v5 on Edge:
// auth.ts — use JWT strategy for Edge compatibility
export const { handlers, auth } = NextAuth({
session: { strategy: "jwt" }, // Required for Edge
// ...providers
});
Which Boilerplates Use Each
| Boilerplate | Auth Choice | Rationale |
|---|---|---|
| T3 Stack | NextAuth v5 | Ecosystem-first, community expectation |
| ShipFast | NextAuth v5 | Simplicity, no extra cost |
| Supastarter | Clerk or Supabase Auth | Choice — B2B users prefer Clerk UI |
| MakerKit | Supabase Auth | Supabase-native architecture |
| Open SaaS | Wasp built-in auth | Framework-native |
| Create T3 Turbo | NextAuth v5 | Monorepo integration |
| Indie Kit | Better Auth | Modern, fee-free |
Better Auth is gaining adoption in 2025–2026 boilerplates as it reaches stability. Expect T3-compatible Better Auth starters to grow throughout 2026.
Decision Guide
Choose Clerk if:
- Speed to auth matters more than cost
- Your team doesn't want to maintain auth infrastructure
- Pre-built UI components save significant design time
- <10,000 MAUs (free tier covers most early-stage SaaS)
- B2B SaaS where organization management UI is needed fast
Choose Better Auth if:
- Self-hosted auth is a requirement (compliance, data residency)
- You need passkeys + 2FA + organizations for free
-
10,000 MAUs where Clerk's per-MAU pricing adds up
- Your team prefers owning the full auth stack
- Framework-agnostic auth that could move to Remix or SvelteKit
Choose NextAuth v5 if:
- You're extending an existing NextAuth v4 project (v5 migration is manageable)
- Maximum OAuth provider support is required
- Your team knows NextAuth and values ecosystem familiarity
- Simple auth needs (no 2FA, no orgs, no passkeys)
The Self-Hosted Auth Database Schema Decision
Both Better Auth and NextAuth require you to maintain your own database tables for auth data. The schema decisions you make here have downstream consequences for how you query user data, how you enforce access control, and how you audit auth events.
Better Auth's generated schema includes tables for user, session, account, verification, and organization data. The schema is opinionated but well-designed — the relationships are correct, the indexes are included, and the column names are descriptive. For most SaaS products, using Better Auth's generated schema as-is is the right approach. Customizing it is possible but creates drift that makes future Better Auth updates harder to apply.
NextAuth's schema (via the Drizzle or Prisma adapter) is minimal by design — users, sessions, accounts, and verification tokens. If you need additional columns on the user table (role, subscription status, organization ID), you add them through the adapter's type extension. This is well-documented but requires understanding the adapter pattern to avoid breaking NextAuth's session callbacks.
The practical consequence of both: your user table is shared between auth infrastructure code and your application business logic. A user's subscription status, role, and application-specific data share a table with their email, password hash, and auth tokens. This coupling is normal and expected — it's just worth knowing before you design your database schema that the auth library owns some columns and your application code owns others. Keeping auth library columns immutable in your application code (let the library manage them) avoids hard-to-debug state inconsistencies.
Passkeys in Production: What "Built-In" Really Means
Both Clerk and Better Auth advertise passkey support, but the user experience of implementing passkeys differs significantly.
Clerk's passkeys are included in the pre-built <SignIn /> and <SignUp /> components. Users see passkey prompts automatically on supporting devices. There's no additional code to write. The trade-off is that you don't control the UI or UX of the passkey registration flow — it's whatever Clerk's component renders.
Better Auth's passkey plugin provides the server-side WebAuthn implementation and client-side API hooks. You're responsible for building the UI that prompts users to register a passkey, the button that initiates the WebAuthn ceremony, and the error handling for browsers or devices that don't support passkeys. This is more work but gives you a passkey experience that matches your product's design language.
In 2026, passkeys are not yet mainstream user expectations in most SaaS categories. Building custom passkey UI requires non-trivial effort for a feature many users won't discover or use in the first year. For most SaaS products, Clerk's handled passkey implementation is sufficient. For products where passwordless auth is a core value proposition — security tools, identity products, enterprise software — custom passkey UX may be worth the implementation investment.
See our Clerk vs Auth0 vs WorkOS comparison for the enterprise auth provider perspective.
Browse best SaaS boilerplates by auth provider on StarterPick.
Review the boilerplate buyers checklist for evaluating auth implementation quality in any starter kit.