Skip to main content

Auth.js v5 vs Lucia v3 vs Better Auth 2026

·StarterPick Team
Share:

Auth Is Not Solved — It Is Just Complicated Differently

Authentication in Next.js in 2026 means choosing between three competing philosophies: Auth.js (the established standard), Lucia (the "bring your own database" approach), and Better Auth (the comprehensive newcomer). Each makes different trade-offs.

The right choice depends on your stack, your complexity tolerance, and what you are building.

TL;DR

  • Auth.js v5 (NextAuth): Choose for OAuth-first apps (Google, GitHub login), rapid prototypes, and teams that know it. Established, broad adapter support.
  • Lucia v3: Deprecated as a library since September 2024. The session patterns are still valuable as architecture guidance, but use Better Auth or implement sessions manually instead.
  • Better Auth: Choose for feature-complete auth with organizations, 2FA, and magic links out of the box. The rising standard.
  • Clerk: Ignore for self-hosted; use for managed auth when you want to pay to not think about auth ($25+/mo).

Key Takeaways

  • Auth.js v5 is the most used (200K+ weekly npm installs) but v5 was a breaking rewrite
  • Lucia was deprecated in September 2024 as a library — the author now recommends it as a "learning resource" and advocates for the patterns, not the package
  • Better Auth launched in 2024 and has grown rapidly (~50K weekly downloads); the most feature-complete self-hosted option
  • All three support Next.js App Router server components and server actions
  • Better Auth is the current recommendation for new projects in 2026

Auth.js v5 (NextAuth)

Auth.js v5 is the fifth major version of NextAuth.js. It introduced edge compatibility, server actions support, and a redesigned API.

Setup

npm install next-auth@beta @auth/prisma-adapter
// auth.ts — v5 configuration:
import NextAuth from 'next-auth';
import { PrismaAdapter } from '@auth/prisma-adapter';
import Google from 'next-auth/providers/google';
import Resend from 'next-auth/providers/resend';
import { db } from './lib/db';

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(db),
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID!,
      clientSecret: process.env.AUTH_GOOGLE_SECRET!,
    }),
    Resend({
      from: 'noreply@yourapp.com',
    }),
  ],
  callbacks: {
    session({ session, user }) {
      session.user.id = user.id;
      session.user.role = user.role;
      return session;
    },
  },
});

// app/api/auth/[...nextauth]/route.ts:
export { handlers as GET, handlers as POST };
// Using auth in server components:
import { auth } from '@/auth';

export default async function DashboardPage() {
  const session = await auth();
  if (!session) redirect('/login');

  return <div>Hello, {session.user.name}</div>;
}

// Using auth in server actions:
export async function updateProfile(data: FormData) {
  'use server';
  const session = await auth();
  if (!session) throw new Error('Unauthorized');

  await db.user.update({
    where: { id: session.user.id },
    data: { name: data.get('name') as string },
  });
}

Auth.js v5 Strengths

  • 20+ built-in OAuth providers (Google, GitHub, Discord, Apple, etc.)
  • Active community and extensive documentation
  • Prisma, Drizzle, MongoDB adapters maintained
  • Magic links (email) built-in
  • JWTs or database sessions

Auth.js v5 Weaknesses

  • v5 is still in beta (as of March 2026) — API may change
  • No built-in organization/team support
  • No built-in 2FA
  • Session refresh logic can be tricky

Lucia v3 (Reference Implementation)

Lucia v3 is no longer recommended as a production library. In September 2024, the author deprecated it and shifted to providing auth as educational patterns. The Lucia approach is still valid as architecture guidance.

The Lucia pattern: implement sessions yourself using the concepts Lucia pioneered.

// The "Lucia pattern" — manual session management:
import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import { db } from './db';

export function generateSessionToken(): string {
  const bytes = new Uint8Array(20);
  crypto.getRandomValues(bytes);
  return encodeBase32LowerCaseNoPadding(bytes);
}

export async function createSession(token: string, userId: string) {
  const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
  const session = {
    id: sessionId,
    userId,
    expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),  // 30 days
  };
  await db.session.create({ data: session });
  return session;
}

export async function validateSessionToken(token: string) {
  const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
  const session = await db.session.findUnique({
    where: { id: sessionId },
    include: { user: true },
  });

  if (!session || Date.now() >= session.expiresAt.getTime()) {
    if (session) await db.session.delete({ where: { id: sessionId } });
    return { session: null, user: null };
  }

  // Refresh session if expiring in less than 15 days:
  if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) {
    session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
    await db.session.update({
      where: { id: sessionId },
      data: { expiresAt: session.expiresAt },
    });
  }

  return { session, user: session.user };
}

Use the Lucia patterns if: You want full control and understand auth deeply. Not recommended for teams who want to ship fast.


Better Auth launched in 2024 as a comprehensive auth solution with features that others charge for or lack entirely.

npm install better-auth
// auth.ts — Better Auth configuration:
import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
import { organization } from 'better-auth/plugins';
import { twoFactor } from 'better-auth/plugins';
import { db } from './lib/db';

export const auth = betterAuth({
  database: prismaAdapter(db, { provider: 'postgresql' }),
  emailAndPassword: { enabled: 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: [
    organization(),   // Teams, roles, invitations
    twoFactor(),      // TOTP 2FA
  ],
});

// Route handler:
// app/api/auth/[...all]/route.ts:
import { auth } from '@/auth';
import { toNextJsHandler } from 'better-auth/next-js';
export const { GET, POST } = toNextJsHandler(auth);
// Client-side usage:
import { createAuthClient } from 'better-auth/react';
import { organizationClient } from 'better-auth/client/plugins';
import { twoFactorClient } from 'better-auth/client/plugins';

export const { signIn, signOut, useSession, organization } = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_URL!,
  plugins: [organizationClient(), twoFactorClient()],
});

// In a React component:
const { data: session } = useSession();

// Sign in with organization support:
await signIn.social({ provider: 'google', callbackURL: '/dashboard' });

// Create an organization:
await organization.create({ name: 'Acme Corp', slug: 'acme' });

Better Auth Strengths

  • Organizations/teams built-in — roles, permissions, invitations
  • Two-factor authentication (TOTP) built-in
  • Magic links built-in
  • Passkeys support
  • Rate limiting on auth endpoints
  • Active development with weekly releases
  • TypeScript-first with excellent type inference

Better Auth Weaknesses

  • Newer library — smaller community than Auth.js
  • Documentation still maturing
  • Fewer examples in the wild

Feature Comparison

FeatureAuth.js v5Lucia PatternBetter Auth
OAuth providers20+ built-inManual10+ built-in
Email/passwordVia providerManualBuilt-in
Magic linksVia Resend providerManualBuilt-in
Organizations/teamsNoManualBuilt-in plugin
Two-factor authNoManualBuilt-in plugin
PasskeysNoManualBuilt-in
Session handlingAutomaticManualAutomatic
Drizzle ORMYesYesYes
Prisma ORMYesYesYes
Edge compatibleYesYesYes
Weekly downloads~200KDeprecated~50K

Which Boilerplates Use Which?

BoilerplateDefault Auth
ShipFastAuth.js (NextAuth)
MakerkitBetter Auth or Supabase Auth
SupastarterSupabase Auth
SaaSBoldAuth.js
OpenSaaSWasp auth (Passport.js)
T3 StackAuth.js or NextAuth
Midday v1Supabase Auth

Recommendation

For new projects in 2026: Better Auth.

It provides the most features without sacrificing developer experience. Organizations, 2FA, and magic links are common SaaS requirements. Better Auth includes all three. Auth.js is still a solid choice if your team knows it well, but for new projects, Better Auth is the better starting point.

For existing projects: Keep Auth.js unless you need organizations or 2FA. Migration from Auth.js to Better Auth is non-trivial.

For maximum control: Use the Lucia patterns (implement sessions yourself using the published algorithms) if you need deep customization.

Methodology

Based on publicly available documentation from Auth.js, Lucia, and Better Auth, npm download statistics, and community discussions as of March 2026.


Choosing an auth strategy for your SaaS? StarterPick helps you find boilerplates pre-configured with the right auth library for your needs.

Session Security: What Each Library Handles Automatically

Authentication is not just about logging users in — it's about maintaining secure sessions over time and handling edge cases that cause security bugs. The libraries differ meaningfully in what they handle automatically vs what they leave to the developer.

Auth.js v5 handles session security through signed JWTs or database sessions depending on your adapter configuration. With the database session strategy (recommended for most SaaS), sessions are stored in a sessions table with an expiry timestamp. Auth.js rotates the session cookie on each request if configured, which prevents session fixation attacks. CSRF protection is built in for form submissions. The edge-compatible session reading uses the JWT strategy by default because database lookups in Edge Middleware are constrained — this means your Edge Middleware reads a signed JWT, not a database session, and a revoked session won't be blocked at the edge until the JWT expires.

Better Auth's session management is more modern. Sessions are database-backed with sliding expiration (sessions extend automatically when a user is active, without changing the session ID). CSRF protection uses both the SameSite cookie attribute and origin header validation. The organization plugin adds session context that includes the user's current organization and role — available in every request without an additional database query. Passkey sessions use a different token flow but are handled transparently by the passkey plugin.

The Lucia pattern (implementing sessions manually) requires you to decide on all of these behaviors explicitly. The session token hashing (SHA-256 before storage), the expiry logic, the sliding vs fixed window behavior, and the cookie security flags are all configuration you write. This gives maximum control but maximum opportunity for mistakes. The Lucia documentation provides the recommended patterns, but it doesn't enforce them — you can implement a secure Lucia-style session system, or an insecure one, with equal ease.

The practical guidance: for teams without deep auth security experience, Auth.js or Better Auth are significantly safer choices than the Lucia patterns. The security properties that Auth.js and Better Auth provide automatically took years of community review to develop. Implementing sessions manually should be reserved for teams with explicit auth security expertise and a specific reason why the library options are insufficient.

Multi-Tenancy Support: The Organizational Layer

For B2B SaaS, the organization layer is one of the most important auth features — and where Auth.js falls behind.

A B2B SaaS product almost always needs: users belonging to one or more organizations, roles within organizations (owner, admin, member), invitation flows for adding teammates, and the ability to switch between organizations. Auth.js v5 has no built-in concept of organizations. You implement this in your application layer — typically a database schema with Organization, OrganizationMember, and Invitation tables, plus middleware that attaches the current organization to the request context.

Building organization support on top of Auth.js is well-documented and common, but it typically takes 5–10 days for a robust implementation with invitation emails, role enforcement at the API level, and organization switching in the UI.

Better Auth's organization plugin ships all of this. The organization() plugin adds organization CRUD, member management, role definitions, and invitation handling to your auth system. The client-side organization object provides methods for creating organizations, inviting members, switching the active organization, and managing roles. The schema migration that Better Auth generates creates the required tables automatically.

The reason Better Auth is the recommended starting point for new B2B products in 2026 is precisely this: organizations are required for B2B SaaS, and shipping them in a weekend vs a week-and-a-half makes a real difference in time-to-value. If your product serves individuals (B2C, tools, utilities), Auth.js and Better Auth are equivalently suitable. If your product serves teams, Better Auth starts ahead.

Clerk vs Self-Hosted: The Managed Auth Trade-Off

The discussion of self-hosted auth libraries (Auth.js, Better Auth, Lucia patterns) often omits the managed alternative: Clerk.

Clerk is a managed auth service. You use Clerk's API, Clerk's pre-built UI components (sign-in, sign-up, organization management, user profile), and Clerk handles all session management, security patching, and infrastructure. The pricing is $0 for up to 10,000 MAU, then $0.02/MAU — so a product with 50,000 MAU pays $800/month just for auth.

When Clerk wins: your team is moving extremely fast and doesn't want to maintain auth code, you want pre-built UI that matches Clerk's clean design system, you're certain you'll stay under 10K MAU for the foreseeable future, or you're building a product where your users are non-technical and auth polish matters more than auth flexibility.

When self-hosted wins: you scale past 10K MAU (the cost becomes significant), you have data residency requirements that prevent user data from going to Clerk's servers, you need auth behaviors that Clerk doesn't support (custom session logic, unusual OAuth providers), or you're building a product where the auth UI must be deeply integrated with your product's design system.

The decision is reversible but not cheap. Migrating from Clerk to Better Auth requires moving session management, updating all auth checks in your application, and potentially migrating user IDs (Clerk's user IDs are Clerk-specific; your database records likely use Clerk user IDs as foreign keys). Budget 5–10 days for a complete migration. Make the Clerk vs self-hosted decision based on your 18-month MAU projection and data requirements, not just what's easiest to set up in week one.


See the Next.js SaaS tech stack guide for the full auth context within the recommended 2026 stack.

Compare boilerplates by their auth implementation in the best SaaS boilerplates 2026 guide.

Read the ShipFast review to see how Auth.js is configured in one of the most widely-used commercial boilerplates.

See the best Next.js boilerplates guide for the full landscape of starters with their auth choices side by side.

Auth Migration Complexity in Practice

Migrating between auth systems after launch is one of the most disruptive changes you can make to a production SaaS. The user impact is significant: all active sessions are invalidated, users need to reset passwords if you are changing credential storage formats, and any OAuth tokens need to be re-authorized. This is worth doing when the current system has a serious limitation, but it should not be done casually.

Auth.js to Better Auth: The session table format is different, but both use similar database schema patterns. The migration requires a new sessions table, rewriting your session reading logic in middleware, and updating all places where auth() or getServerSession() is called. For a medium-sized codebase, this is five to seven days of focused work. User passwords stored with bcrypt are compatible — you keep the passwords table and hash format. OAuth accounts need to be migrated to Better Auth's accounts table format, which requires a data migration script. Users do not need to re-authenticate if you migrate the accounts table correctly.

Better Auth to a managed auth service (Clerk): This migration is more complex because you are moving from self-managed user IDs to Clerk's user IDs. Every record in your database that uses a user ID as a foreign key needs updating. Clerk provides a migration API that allows bulk user import, but the user ID change requires either a database migration to update foreign keys or a mapping table that translates Clerk IDs to legacy IDs. Budget ten to fifteen days for a complete migration including testing.

The cleanest scenario: use Better Auth from the start of a new project. Its organization plugin covers B2B requirements, its schema is clean, and its migration path to a managed service (if you ever need to) is more predictable than migrating from Auth.js, which has a more complex session format.

The SaaS Boilerplate Matrix (Free PDF)

20+ SaaS starters compared: pricing, tech stack, auth, payments, and what you actually ship with. Updated monthly. Used by 150+ founders.

Join 150+ SaaS founders. Unsubscribe in one click.