Lucia Auth v3 vs Better Auth vs Stack Auth 2026
Lucia Auth v3 vs Better Auth vs Stack Auth: Self-Hosted Auth 2026
TL;DR
The "auth-as-a-service" era is being challenged. Developers tired of Auth0's pricing, Clerk's vendor lock-in, and Firebase Auth's Google dependency are reaching for self-hosted alternatives. Lucia Auth v3 is the authentication framework — not a library of pre-built flows, but a set of utilities for building session-based auth your way; it gives you full control but requires you to wire up OAuth, database storage, and session middleware yourself. Better Auth is the full-featured framework — comprehensive built-in plugins for OAuth, 2FA, magic links, passkeys, and organizations, with adapters for Drizzle, Prisma, and MongoDB; it's designed to be the self-hosted Clerk. Stack Auth is the open-source Clerk alternative — a hosted (or self-hostable) auth service with a pre-built UI component library and a full user management dashboard; it's the closest drop-in replacement for Clerk with no pricing cliff. For maximum control over auth primitives: Lucia. For feature-complete self-hosted auth without building flows: Better Auth. For teams that want Clerk's UX without Clerk's pricing: Stack Auth.
Key Takeaways
- Lucia v3 is a framework, not a service — you build auth using its primitives, not pre-built flows
- Better Auth has 30+ plugins — OAuth, 2FA, magic links, passkeys, organizations, admin, RBAC
- Stack Auth includes a hosted dashboard — user management UI out of the box
- Lucia is database-agnostic — adapters for PostgreSQL, MySQL, SQLite, MongoDB, Redis
- Better Auth supports multi-tenancy — organization support with member roles and invitations
- Stack Auth is open-source — self-host or use their managed cloud ($0 to start)
- All three are TypeScript-first — full type safety end to end
Why Self-Hosted Auth?
Auth-as-a-service pricing reality:
Clerk: $0 up to 10k MAU → $25/month for 10k-20k → can reach $1,000+/month at scale
Auth0: $0 up to 7,500 MAU → $240/month for 7,500+ users
Firebase: $0 for most cases → costs emerge with SMS, advanced features
Self-hosted alternatives:
One-time setup → marginal cost of your database + compute
Full ownership of user data (GDPR, data residency)
No pricing cliff at scale milestones
No vendor API changes breaking your auth flow
Custom session behavior, token formats, and storage
Lucia Auth v3: Auth Primitives Framework
Lucia v3 is a library for building session-based authentication — it handles session ID generation, database storage, cookie management, and CSRF protection, but you implement the user model, OAuth flow, and UI.
Installation
npm install lucia
# Database adapter (pick one)
npm install @lucia-auth/adapter-drizzle
# npm install @lucia-auth/adapter-prisma
# npm install @lucia-auth/adapter-sqlite
Core Setup with Drizzle
// lib/auth.ts
import { Lucia } from "lucia";
import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle";
import { db } from "./db";
import { sessions, users } from "./schema";
// Adapter links Lucia to your database schema
const adapter = new DrizzleSQLiteAdapter(db, sessions, users);
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: process.env.NODE_ENV === "production",
},
},
getUserAttributes: (attributes) => ({
// Expose user attributes to session — typed
email: attributes.email,
username: attributes.username,
role: attributes.role,
}),
});
// TypeScript augmentation for full type safety
declare module "lucia" {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: {
email: string;
username: string;
role: string;
};
}
}
Database Schema (Drizzle SQLite)
// lib/schema.ts
import { sqliteTable, text, integer, blob } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: text("id").primaryKey(),
email: text("email").notNull().unique(),
username: text("username").notNull(),
hashedPassword: text("hashed_password"),
role: text("role").notNull().default("user"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
});
export const sessions = sqliteTable("sessions", {
id: text("id").primaryKey(),
userId: text("user_id").notNull().references(() => users.id),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
});
// OAuth accounts (for OAuth providers)
export const oauthAccounts = sqliteTable("oauth_accounts", {
providerName: text("provider_name").notNull(),
providerUserId: text("provider_user_id").notNull(),
userId: text("user_id").notNull().references(() => users.id),
});
Email + Password Sign Up
// app/api/auth/signup/route.ts
import { lucia } from "@/lib/auth";
import { db } from "@/lib/db";
import { users } from "@/lib/schema";
import { hash } from "@node-rs/argon2";
import { generateId } from "lucia";
import { cookies } from "next/headers";
export async function POST(req: Request) {
const { email, password, username } = await req.json();
// Validate inputs
if (!email || !password || password.length < 8) {
return Response.json({ error: "Invalid input" }, { status: 400 });
}
// Hash password with Argon2 (recommended for auth)
const hashedPassword = await hash(password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
const userId = generateId(15); // Lucia's secure ID generator
// Insert user
await db.insert(users).values({
id: userId,
email,
username,
hashedPassword,
role: "user",
createdAt: new Date(),
});
// Create session
const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);
const cookieStore = await cookies();
cookieStore.set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes
);
return Response.json({ success: true });
}
Session Validation Middleware
// lib/auth-utils.ts
import { lucia } from "@/lib/auth";
import { cookies } from "next/headers";
import { cache } from "react";
// Cached per request — safe to call multiple times
export const validateRequest = cache(async () => {
const cookieStore = await cookies();
const sessionId = cookieStore.get(lucia.sessionCookieName)?.value ?? null;
if (!sessionId) return { user: null, session: null };
const { user, session } = await lucia.validateSession(sessionId);
// Refresh session cookie if it's close to expiry
if (session?.fresh) {
const sessionCookie = lucia.createSessionCookie(session.id);
const store = await cookies();
store.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
// Clear cookie on invalid session
if (!session) {
const blankCookie = lucia.createBlankSessionCookie();
const store = await cookies();
store.set(blankCookie.name, blankCookie.value, blankCookie.attributes);
}
return { user, session };
});
// Next.js middleware (middleware.ts)
export async function authMiddleware(req: NextRequest) {
const { user } = await validateRequest();
if (!user && req.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", req.url));
}
}
GitHub OAuth
import { GitHub, generateState } from "arctic"; // Arctic = OAuth 2.0 library for Lucia
const github = new GitHub(
process.env.GITHUB_CLIENT_ID!,
process.env.GITHUB_CLIENT_SECRET!
);
// app/api/auth/github/route.ts
export async function GET() {
const state = generateState();
const url = await github.createAuthorizationURL(state, { scopes: ["user:email"] });
const cookieStore = await cookies();
cookieStore.set("github_oauth_state", state, {
path: "/",
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 60 * 10,
sameSite: "lax",
});
return Response.redirect(url.toString());
}
// app/api/auth/github/callback/route.ts
export async function GET(req: Request) {
const url = new URL(req.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const cookieStore = await cookies();
const storedState = cookieStore.get("github_oauth_state")?.value;
if (!code || !state || state !== storedState) {
return new Response("Invalid state", { status: 400 });
}
const tokens = await github.validateAuthorizationCode(code);
const githubUser = await fetchGitHubUser(tokens.accessToken());
// Upsert user and create session
const userId = await upsertGitHubUser(githubUser);
const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookieStore.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return Response.redirect(new URL("/dashboard", req.url));
}
Better Auth: Full-Featured Auth Framework
Better Auth provides a complete auth solution — sign-in methods, session management, and plugins for organizations, 2FA, passkeys, and more — all self-hosted.
Installation
npm install better-auth
Server Setup
// lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { twoFactor, organization, passkey, magicLink } from "better-auth/plugins";
import { db } from "./db";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg", // "pg" | "sqlite" | "mysql"
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendResetPassword: async ({ user, url }) => {
await sendEmail({ to: user.email, subject: "Reset password", html: `<a href="${url}">Reset</a>` });
},
},
emailVerification: {
sendOnSignUp: true,
sendVerificationEmail: async ({ user, url }) => {
await sendEmail({ to: user.email, subject: "Verify your email", html: `<a href="${url}">Verify</a>` });
},
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
plugins: [
twoFactor({
issuer: "MyApp",
otpOptions: { digits: 6 },
}),
organization({
allowUserToCreateOrganization: true,
organizationLimit: 5,
}),
passkey({
rpName: "My App",
rpID: process.env.NEXT_PUBLIC_APP_URL!,
}),
magicLink({
sendMagicLink: async ({ email, url }) => {
await sendEmail({ to: email, subject: "Sign in to MyApp", html: `<a href="${url}">Sign in</a>` });
},
}),
],
});
export type Auth = typeof auth;
Next.js Route Handler
// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);
Client Setup and Sign-In
// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { twoFactorClient, organizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL!,
plugins: [twoFactorClient(), organizationClient()],
});
export const {
signIn,
signUp,
signOut,
useSession,
organization,
} = authClient;
// components/SignInForm.tsx
import { signIn, signUp } from "@/lib/auth-client";
function SignInForm() {
const handleEmailSignIn = async (email: string, password: string) => {
await signIn.email({ email, password, callbackURL: "/dashboard" });
};
const handleGitHubSignIn = async () => {
await signIn.social({ provider: "github", callbackURL: "/dashboard" });
};
const handleMagicLink = async (email: string) => {
await signIn.magicLink({ email, callbackURL: "/dashboard" });
};
return (
<div>
<button onClick={() => handleGitHubSignIn()}>Sign in with GitHub</button>
<button onClick={() => handleMagicLink("user@example.com")}>Send Magic Link</button>
</div>
);
}
Organizations and RBAC
import { organization } from "@/lib/auth-client";
// Create organization
const org = await organization.create({
name: "Acme Corp",
slug: "acme-corp",
});
// Invite member
await organization.inviteMember({
email: "colleague@example.com",
role: "member", // "owner" | "admin" | "member"
organizationId: org.id,
});
// Check member permissions
const member = await organization.getActiveMember();
const canInvite = member?.role === "owner" || member?.role === "admin";
Stack Auth: Open-Source Clerk Alternative
Stack Auth provides a hosted (or self-hostable) auth service with pre-built UI components and a user management dashboard — the closest open-source equivalent to Clerk.
Installation
npm install @stackframe/stack
Server Setup
// stack.ts
import { StackServerApp } from "@stackframe/stack";
export const stackServerApp = new StackServerApp({
tokenStore: "nextjs-cookies", // Uses Next.js cookies for session storage
// Or: tokenStore: "cookie" for other frameworks
});
Next.js App Router Integration
// app/layout.tsx
import { StackProvider, StackTheme } from "@stackframe/stack";
import { stackServerApp } from "../stack";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<StackProvider app={stackServerApp}>
<StackTheme>
{children}
</StackTheme>
</StackProvider>
</body>
</html>
);
}
Pre-Built Auth UI Components
// Stack Auth ships pre-built, customizable auth pages
import { SignIn, SignUp, UserButton, AccountSettings } from "@stackframe/stack";
// Drop-in sign-in page — handles OAuth, email/password, magic links
function SignInPage() {
return (
<div>
<SignIn
fullPage={true}
automaticRedirect={true}
afterSignIn="/dashboard"
/>
</div>
);
}
// User avatar button with session management menu
function Header() {
return (
<nav>
<UserButton />
</nav>
);
}
// Full account settings page
function SettingsPage() {
return <AccountSettings fullPage={true} />;
}
Access Current User
import { useUser, useStackApp } from "@stackframe/stack";
import { stackServerApp } from "@/stack";
// Client component
function UserDisplay() {
const user = useUser({ or: "redirect" }); // Redirects to sign-in if not authenticated
return (
<div>
<p>Hello, {user.displayName}</p>
<p>Email: {user.primaryEmail}</p>
</div>
);
}
// Server component
async function ServerUserDisplay() {
const user = await stackServerApp.getUser({ or: "redirect" });
return <p>Hello, {user.displayName}</p>;
}
// Get current user server-side
async function getCurrentUser() {
const user = await stackServerApp.getUser();
return user; // null if not authenticated
}
User Management and Admin
// Server-side user management
const users = await stackServerApp.listUsers();
// Get specific user by ID
const user = await stackServerApp.getUser({ id: "user_id" });
// Update user
await user.update({
displayName: "New Name",
clientMetadata: { plan: "premium", company: "Acme" },
});
// Delete user
await user.delete();
Feature Comparison
| Feature | Lucia v3 | Better Auth | Stack Auth |
|---|---|---|---|
| Type | Auth primitives | Full framework | Auth service (OSS) |
| Setup complexity | High | Medium | Low |
| Pre-built UI | ❌ | ❌ (headless) | ✅ Components |
| Email + Password | Manual | ✅ | ✅ |
| OAuth providers | Via Arctic | ✅ 20+ | ✅ 20+ |
| Magic links | Manual | ✅ Plugin | ✅ |
| Passkeys / WebAuthn | Manual | ✅ Plugin | ✅ |
| 2FA / TOTP | Manual | ✅ Plugin | ✅ |
| Organizations | Manual | ✅ Plugin | ✅ Teams |
| Admin dashboard | ❌ | ❌ | ✅ Hosted |
| DB adapters | Drizzle, Prisma, more | Drizzle, Prisma, Mongo | Managed / Neon |
| Self-hostable | ✅ | ✅ | ✅ |
| GitHub stars | 5.1k | 6.2k | 2.9k |
When to Use Each
Choose Lucia Auth v3 if:
- You want full control over every aspect of auth (session format, token storage, cookie behavior)
- Building a custom auth flow that doesn't fit standard patterns
- You prefer to own all auth logic and don't want framework magic
- Academic understanding of session-based auth — Lucia's source code is educational
Choose Better Auth if:
- You want a complete self-hosted auth system without building OAuth flows from scratch
- Multi-tenant organization support with member roles is required
- Passkeys, 2FA, and magic links are needed out of the box
- You're replacing Clerk/Auth0 but want to keep all their features
Choose Stack Auth if:
- You're replacing Clerk and want the closest UX match (pre-built components, hosted dashboard)
- Your team doesn't want to build auth UI — drop in
<SignIn />and you're done - User management dashboard is important (customer support team needs to manage users)
- Open-source but hosted option — start with Stack Auth cloud, self-host later if needed
Ecosystem & Community
The self-hosted auth ecosystem has exploded since 2023, driven primarily by Clerk's pricing changes that caught many startups off guard at scale. This created real demand for credible alternatives, and all three libraries benefited from developer attention that would previously have defaulted to hosted services.
Better Auth's GitHub star trajectory has been one of the steepest in the authentication space — from virtually unknown in early 2024 to 6,200 stars by February 2026. This rapid growth reflects genuine community enthusiasm for a library that matches the DX quality of hosted solutions while remaining fully self-hosted. The Discord community is active and the maintainer is responsive to issues. Better Auth's plugin architecture has inspired several community-contributed plugins beyond the official set.
Lucia's community is centered around its documentation, which is considered among the best in the authentication space. The documentation doesn't just tell you how to use Lucia — it teaches you how session-based authentication works, making it a valuable resource even for developers who ultimately choose a different library. This educational depth has earned Lucia a devoted following among developers who believe in understanding the fundamentals.
Stack Auth is newer and its community is smaller but growing, particularly among founders and solo developers who want Clerk's ease of use without Clerk's pricing structure. The company behind Stack Auth has raised funding and is actively developing both the open-source project and the hosted platform.
Real-World Adoption
Better Auth has been adopted by a growing number of production applications, particularly in the Next.js ecosystem where its adapter quality and plugin breadth make it a natural fit for SaaS products that need organizations, team management, and multiple auth methods. Several developers have publicly documented migrating from Clerk to Better Auth with favorable cost and control outcomes. For a comparison that includes managed auth services, see best Next.js auth solutions 2026.
Lucia Auth v3 is used as the auth layer in numerous open-source projects where the authors want to demonstrate authentication patterns without introducing opinionated dependencies. Its use in educational resources and starter templates has given it broad awareness even among developers who may choose a higher-level solution for production work.
Stack Auth targets teams that were previously using Clerk and experienced pricing shock. Their case studies demonstrate successful migrations from hosted auth services to Stack Auth Cloud with equivalent features and a more predictable cost model. For B2B SaaS applications that need team/organization features, Stack Auth's built-in team management is a compelling differentiator. For managed authentication alternatives from Firebase and AWS, see Firebase Auth vs AWS Cognito vs Supabase Auth 2026.
Developer Experience Deep Dive
TypeScript support is excellent across all three libraries, but the way type safety is expressed differs meaningfully. Lucia's module augmentation pattern for Register interface is elegant — you define your user attributes once and they flow through the entire type system. The developer experience of knowing exactly what's in your session object without runtime guessing is one of Lucia's genuine strengths.
Better Auth's TypeScript experience benefits from its plugin architecture. When you add the organization plugin, the TypeScript types automatically expand to include organization-related methods and types. This inference-based type extension means you get accurate autocompletion for everything you've enabled without manual type declarations.
Stack Auth provides TypeScript types through its SDK, and the useUser hook's { or: "redirect" } option is a nice DX detail — TypeScript knows that when you pass this option, the returned user is non-null, so you don't need null checks in your component.
Debugging authentication issues is painful with any system. Lucia's advantage here is transparency — because you write the auth logic yourself, you know exactly where to look when something breaks. Better Auth and Stack Auth abstract more away, which is convenient until something goes wrong and you need to understand the internals.
Migration Guide
Migrating from Clerk to Better Auth:
Identify every place in your codebase that uses Clerk's useUser, useAuth, currentUser, and clerkMiddleware. Better Auth's equivalents are useSession, auth server utilities, and middleware helpers. OAuth redirect flows are very similar. The biggest effort is typically migrating user data from Clerk's database to your own — Better Auth provides migration utilities that help import users from common providers.
Migrating from Clerk to Stack Auth:
Stack Auth is designed explicitly for this migration. The component API (<SignIn />, <UserButton />, <AccountSettings />) mirrors Clerk's component model, which means the UI migration is minimal. The server-side user access patterns are also similar. Stack Auth provides a detailed migration guide in their documentation.
Migrating from NextAuth.js to Better Auth or Lucia:
NextAuth.js (now Auth.js) covers similar ground to Better Auth but with a different API philosophy. Better Auth's adapter system accommodates the same databases that Auth.js supports. The migration involves replacing your [...nextauth] route with Better Auth's route handler and updating client-side session access from useSession (next-auth) to Better Auth's equivalent.
Final Verdict 2026
The self-hosted auth space in 2026 offers genuinely viable alternatives to hosted services for the first time. The choice between these three comes down to your team's appetite for building versus configuring versus consuming.
If you want to deeply understand and control your auth implementation, Lucia is the right foundation. If you want a feature-complete auth system you can deploy and forget, Better Auth is the pragmatic choice. If you want to reproduce the Clerk experience with open-source code, Stack Auth gets you there fastest.
For most new Next.js SaaS applications starting in 2026, Better Auth offers the best combination of features, documentation quality, and community support in the self-hosted category.
Methodology
Data sourced from official Lucia v3 documentation (lucia-auth.com), Better Auth documentation (better-auth.com), Stack Auth documentation (stack-auth.com), GitHub star counts as of February 2026, npm download statistics, and community discussions from the Lucia Discord, Better Auth GitHub issues, and r/nextjs.
Related: Best Next.js Auth Solutions 2026, Hono vs Elysia 2026, Turso vs PlanetScale vs Neon Serverless Database 2026
Choosing Confidently
The self-hosted auth space has matured considerably. All three options — Lucia v3, Better Auth, and Stack Auth — are production-capable. The decision comes down to team preference and integration needs.
If you are starting a new Next.js project with App Router and want minimal configuration overhead, Better Auth is the pragmatic choice in 2026. Its session management, OAuth providers, and TypeScript types work correctly out of the box. Lucia v3 remains the right choice for developers who want to understand and control every layer of their session infrastructure. Stack Auth addresses the specific case where you need a hosted control plane and team management without building those features yourself.