Technology·15 min read

Better Auth vs Lucia vs NextAuth: Auth Libraries for Next.js 2026

Self-hosted auth libraries give you zero vendor lock-in and full control over your data. With Lucia in maintenance mode and Better Auth rising fast, the landscape has shifted. Here is how all three actually compare in production.

Nate Laquis

Nate Laquis

Founder & CEO

Why Self-Hosted Auth Libraries Still Matter in 2026

Managed auth providers like Clerk and Auth0 dominate the conversation, and for good reason. They are fast to integrate, handle security patches for you, and remove entire categories of liability from your plate. But managed providers come with real tradeoffs that founders underestimate until it is too late: per-MAU pricing that scales into thousands per month, vendor lock-in that makes switching painful, and black-box behavior you cannot debug when something goes wrong at 2 AM on a Saturday.

Self-hosted auth libraries flip this equation. You own the code, you own the data, and you pay nothing beyond your existing infrastructure costs. Your user credentials never leave your database. You can customize every aspect of the authentication flow without filing support tickets or waiting for a feature to ship on someone else's roadmap. For products in regulated industries, keeping auth data on your own servers can simplify compliance audits dramatically.

Digital security lock representing self-hosted authentication and data ownership

The tradeoff is responsibility. You are on the hook for security updates, session management, token rotation, and every edge case that a managed provider would handle silently. That is a real cost in engineering time and risk. But for teams that have the chops to handle it, self-hosted auth gives you a level of control and cost predictability that no managed provider can match.

Three libraries have defined this space for Next.js developers: NextAuth (now Auth.js), Lucia, and Better Auth. In 2024, Lucia was the community darling for developers who wanted low-level control. By late 2025, Lucia's creator announced the library was entering maintenance mode, leaving thousands of projects looking for a successor. Better Auth stepped into that vacuum with a plugin-based architecture that has quickly become the default recommendation for new projects. Meanwhile, Auth.js (NextAuth v5) continues to evolve as the most widely adopted option, though its design philosophy differs sharply from the other two.

We have shipped production apps with all three at our agency. This is not a feature matrix pulled from READMEs. It is an opinionated breakdown based on real deployments, real migration headaches, and real conversations about which library our team actually wants to use on the next project.

Architecture Comparison: Three Philosophies of Auth

These three libraries look similar on the surface. All three are open source, all three work with Next.js, and all three handle OAuth and sessions. But their architectures reflect fundamentally different philosophies about how auth should work, and understanding these differences will save you from picking the wrong one.

Better Auth: Plugin-Based Composition

Better Auth treats authentication as a composable system. The core library handles the essentials: session management, email/password auth, and basic OAuth. Everything else is a plugin. Need two-factor authentication? Install the 2FA plugin. Need organization and team management? Install the organization plugin. Need magic links, passkeys, or anonymous sessions? There is a plugin for each.

This architecture means your auth layer only includes what you actually use. A simple app with email/password login does not carry the weight of enterprise RBAC code. The plugin system is well-designed, with plugins able to extend the database schema, add API routes, and hook into lifecycle events. Better Auth auto-generates its database tables based on which plugins you enable, which eliminates the tedious manual migration work that plagues other libraries.

Better Auth runs as a standalone API server (often mounted at /api/auth in your Next.js app) and exposes a type-safe client that mirrors the server configuration. If you enable the organization plugin on the server, the client automatically gains organization methods with full TypeScript inference. This is genuinely elegant.

Lucia: Low-Level Primitives

Lucia took the opposite approach. Rather than providing a batteries-included framework, Lucia gave you the smallest possible building blocks: a session manager, a cookie utility, and helper functions for hashing. You wired everything else together yourself. Want OAuth? Lucia pointed you to its companion library Arctic for OAuth client flows, but you wrote the callback handlers, the account linking logic, and the database queries.

This minimalism was Lucia's greatest strength and its fatal flaw. Developers who wanted total control loved it. They understood exactly what every line of their auth code did because they wrote it. But for teams that just wanted auth to work, Lucia meant reinventing solutions to problems that other libraries solved out of the box. The maintenance burden of custom auth code built on Lucia primitives contributed to the creator's decision to step back from active development.

Lucia is now in maintenance mode. It receives security patches but no new features. The official recommendation is to use Better Auth or Auth.js for new projects.

Auth.js (NextAuth v5): Provider-Based Convention

Auth.js is built around the concept of providers and adapters. You configure a list of authentication providers (Google, GitHub, Credentials, Email) and a database adapter (Prisma, Drizzle, etc.), and Auth.js handles the rest. The auth configuration file is the single source of truth, and the library generates routes, handles callbacks, and manages sessions based on that configuration.

This provider-based model works beautifully for the common case: social login with a few OAuth providers, a database session, and basic user management. It falls apart when you need features outside the provider model. Custom authentication flows, multi-tenancy, role-based access, organization management, and two-factor auth all require workarounds that feel bolted on rather than native to the architecture.

Auth.js v5 is a significant improvement over NextAuth v4, with first-class App Router support and Edge Runtime compatibility. But the core philosophy has not changed. Auth.js optimizes for the 80% case of OAuth-based authentication and makes the remaining 20% harder than it should be.

Feature Comparison: OAuth, MFA, and Organization Support

Features matter more than architecture when you are making a practical decision. Here is where the three libraries stand on the capabilities that actually affect your product roadmap.

OAuth and Social Login

Better Auth: Ships with built-in support for major OAuth providers (Google, GitHub, Apple, Microsoft, Discord, Twitter, and dozens more). Adding a provider is a single line in your auth configuration. Better Auth handles the full OAuth flow including PKCE, state validation, and account linking. The social login plugin supports automatic account linking when a user signs in with Google and later tries GitHub using the same email address. This sounds simple but is notoriously tricky to implement correctly.

Auth.js: OAuth is Auth.js's strongest feature. It supports over 80 OAuth providers out of the box with minimal configuration. The provider ecosystem is mature, well-tested, and community-maintained. For pure OAuth use cases, Auth.js is hard to beat. Custom OAuth providers are also straightforward to add.

Lucia: Lucia itself had no built-in OAuth. You used Arctic (a companion library) to handle OAuth client flows and then manually created users and sessions in your database. This gave you complete control but required 50 to 100 lines of callback handler code per provider. With Lucia in maintenance mode, Arctic continues to be maintained independently and can be used with any auth setup.

Email/Password with Verification

Better Auth: Native email/password support with email verification flows built in. The library handles password hashing (using bcrypt or argon2), verification token generation, and expiration. You provide the email sending function, and Better Auth handles the rest. Password reset flows are similarly turnkey.

Auth.js: The Credentials provider supports email/password login, but Auth.js explicitly discourages it. The documentation warns about the security implications and suggests using OAuth or magic links instead. If you do use Credentials, you lose database session support (JWT only), which is a significant limitation. Email verification is not built in and requires custom implementation.

Lucia: You built everything yourself. Lucia provided password hashing utilities, but verification flows, reset tokens, and email delivery were all your responsibility. This is exactly the kind of boilerplate that drove teams away from Lucia toward higher-level alternatives.

Two-Factor Authentication

Better Auth: The 2FA plugin supports TOTP (authenticator apps), backup codes, and trusted devices. Enabling it adds the necessary database columns and API routes automatically. The implementation follows OWASP guidelines and handles edge cases like 2FA enrollment during login, backup code regeneration, and session elevation.

Auth.js: No built-in 2FA support. Implementing TOTP on top of Auth.js requires significant custom work: generating secrets, validating codes, managing backup codes, and extending the session flow to include a 2FA verification step. It is doable but takes 1 to 2 weeks to implement properly and test.

Lucia: Same situation as Auth.js. No built-in 2FA. The community created guides for implementing TOTP with Lucia, but you were writing and maintaining all the code yourself.

Organization and Team Management

Better Auth: The organization plugin adds multi-tenant team support with roles, invitations, and member management. Each user can belong to multiple organizations with different roles. The plugin extends both the database schema and the client API. For B2B SaaS products, this is table stakes, and having it as a first-class plugin rather than a custom build is a genuine time saver.

Auth.js: No built-in organization support. You model organizations in your own database and write middleware to enforce tenant boundaries. This is one of the biggest gaps in Auth.js for B2B use cases.

Lucia: Same as Auth.js. Organizations were entirely custom.

Developer writing authentication code with multiple monitors showing TypeScript

The pattern is clear: Better Auth covers the broadest feature set through its plugin system. Auth.js excels at OAuth but struggles with everything else. Lucia gave you nothing but building blocks, and now those building blocks are in maintenance mode.

Database and ORM Integration

Your auth library needs to talk to your database, and the quality of that integration affects everything from initial setup to long-term maintenance. All three libraries take different approaches to database connectivity, and the differences are more consequential than they appear.

Better Auth: First-Class ORM Support with Auto-Migration

Better Auth supports Prisma, Drizzle, Kysely, and raw SQL (via its built-in Knex-based adapter) out of the box. The standout feature is schema inference. When you configure Better Auth with plugins, it knows exactly what database tables and columns it needs. Run the CLI command and it generates the migration for your ORM of choice. Change your plugin configuration and it generates a new migration reflecting the diff.

With Drizzle, Better Auth can generate the schema file directly. With Prisma, it outputs the schema additions you need to paste into your prisma.schema file. This eliminates an entire class of bugs where your database schema drifts from what the auth library expects.

Better Auth also supports multiple database backends: PostgreSQL, MySQL, SQLite, and MongoDB (via an adapter). The same plugin configuration works regardless of your database, which is useful for teams that run SQLite in development and PostgreSQL in production.

Auth.js: Adapter Pattern

Auth.js uses a formal adapter pattern. Community-maintained adapters exist for Prisma, Drizzle, Kysely, TypeORM, Supabase, D1, and others. You install the adapter package, pass it to your Auth.js configuration, and the library uses it for all database operations.

The adapter interface is well-defined but rigid. Auth.js expects specific table names and column structures (User, Account, Session, VerificationToken). Customizing the schema beyond what Auth.js expects is possible but awkward. Adding custom fields to the User table requires extending the adapter and declaring module augmentation for TypeScript types.

Migration is manual. You create the tables Auth.js needs based on the adapter documentation, and you are responsible for keeping the schema in sync as Auth.js evolves between versions. This has been a persistent pain point, especially during the v4 to v5 migration.

Lucia: Bring Your Own Queries

Lucia had no ORM integration. It defined a minimal interface (store a session, retrieve a session, delete a session) and you implemented it with whatever database library you preferred. This was maximally flexible but meant writing and testing database queries for every auth operation. Common patterns were documented, but you owned the code.

For teams using Prisma or Drizzle, community adapters existed but were third-party maintained and occasionally fell behind Lucia's releases. The lack of official ORM support was a friction point for teams coming from NextAuth, where adapters are a first-class concept.

Session Storage: JWT vs Database

This is where architectural choices create real product implications. Better Auth defaults to database sessions with a configurable session table. Every session is stored in your database, which means you can revoke sessions instantly, see all active sessions for a user, and enforce concurrent session limits. The session token in the cookie is an opaque ID that maps to the database record.

Auth.js defaults to JWT sessions. The session data is encoded in a signed JWT stored in a cookie. This is stateless and works on edge runtimes without database access, but it means you cannot revoke sessions without a blocklist, and session data is limited to what fits in a cookie. Auth.js supports database sessions as an alternative, but the Credentials provider does not work with database sessions, which is a significant limitation that has frustrated developers for years.

Lucia used database sessions exclusively. There was no JWT option. This was an opinionated choice that prioritized security and session control over edge runtime compatibility.

Our recommendation: use database sessions unless you have a specific technical reason to use JWTs (like running auth on Cloudflare Workers where database latency is a concern). Database sessions are more secure, more flexible, and easier to debug. If you are evaluating these libraries while building secure authentication, database sessions should be your default.

TypeScript Experience and Developer Ergonomics

TypeScript support is not just a checkbox. The quality of type inference affects how fast your team ships, how many bugs reach production, and how much your developers enjoy working with the auth layer. All three libraries are written in TypeScript, but the experience varies wildly.

Better Auth: End-to-End Type Safety

Better Auth's type system is its most impressive technical achievement. The server configuration drives the client types through generic inference. When you add the organization plugin to your server config, the client automatically knows about organization methods, and your IDE autocompletes them correctly. When you add custom fields to your user schema, those fields appear in session types throughout your application.

The auth client is created with a single function call that infers its type from the server configuration. This means you get compile-time errors if you try to call a method from a plugin you have not installed. It also means refactoring is safe: remove a plugin from the server and TypeScript flags every client call that no longer exists.

Better Auth also provides typed middleware helpers for Next.js. The session object in your API routes and server components has the correct type based on your configuration, including custom user fields and organization data. No manual type assertions or module augmentation required.

Auth.js: Module Augmentation Pain

Auth.js ships with TypeScript support, but extending types requires module augmentation. Want to add a "role" field to your session? You declare a module augmentation for "next-auth" that extends the Session and User interfaces. This works but is clunky, poorly discoverable, and breaks when Auth.js updates its type definitions.

The provider types are well-maintained, and the configuration object has good type checking. But the runtime types (what you get from getServerSession or useSession) require manual extension for any custom data. The gap between what the library knows at configuration time and what it exposes to your application code is a constant source of friction.

Auth.js v5 improved the TypeScript story compared to v4, but it is still a generation behind Better Auth's inference-based approach. You spend more time fighting types than leveraging them.

Lucia: Types Were Your Problem

Lucia provided generic types for its core primitives (Session, User) that you parameterized with your own database schema. This gave you accurate types for session data, but everything outside Lucia's core (OAuth callbacks, verification flows, organization logic) had whatever types you defined yourself.

For experienced TypeScript developers, this was fine. You knew how to type your auth utilities correctly. For teams with mixed TypeScript proficiency, the lack of guardrails led to type assertions (the dreaded "as any") scattered throughout auth code. Lucia was a skilled developer's tool that punished less experienced teams.

Laptop showing TypeScript code with IDE autocompletion for authentication library

The bottom line: Better Auth's type inference is a genuine productivity multiplier. Auth.js types are functional but require manual work. Lucia types depended entirely on your own discipline. If TypeScript developer experience is a priority for your team, Better Auth is the clear winner.

Migration Paths: Moving Between Libraries

Whether you are migrating away from Lucia before it stops receiving security patches, upgrading from NextAuth v4 to Auth.js v5, or switching to Better Auth for its plugin ecosystem, migration is a real concern. Here are the practical paths we have used on client projects.

Migrating from Lucia to Better Auth

This is the most common migration we see in 2026, and for good reason. Lucia is in maintenance mode, and Better Auth is the spiritual successor that covers the same "own your auth" philosophy with significantly less boilerplate.

The good news: if you used Lucia with database sessions (which was the only option), your session table structure is similar to what Better Auth expects. The migration involves creating Better Auth's expected tables (or mapping your existing ones), moving user and session data, and replacing Lucia's API calls with Better Auth's client methods.

Password hashes are compatible if you used Lucia's built-in hashing (bcrypt or scrypt). Better Auth supports both. Users will not need to reset passwords.

The main effort is replacing custom code. Everything you built manually with Lucia (OAuth callbacks, email verification, 2FA) gets replaced by Better Auth plugins. This is a net reduction in code, but it requires understanding how each plugin works and testing the new flows thoroughly. Budget 1 to 3 weeks depending on how much custom auth logic you built on top of Lucia.

Migrating from NextAuth/Auth.js to Better Auth

This migration is more involved because the architectures differ significantly. Auth.js uses a provider/adapter model where configuration drives behavior. Better Auth uses a plugin model where you compose features explicitly.

Step one: map your Auth.js providers to Better Auth equivalents. OAuth providers translate directly. If you used the Credentials provider for email/password, you switch to Better Auth's built-in email/password support, which is actually an upgrade since you get proper database sessions.

Step two: database migration. Auth.js and Better Auth use different table structures. Auth.js uses an Account table that stores OAuth tokens per provider. Better Auth uses a similar structure but with different column names and relationships. Write a migration script that transforms the data. We have open-sourced a migration helper for Prisma-based projects that handles the common cases.

Step three: replace session handling. Auth.js getServerSession calls become Better Auth session calls. If you were using JWT sessions with Auth.js, you are moving to database sessions with Better Auth. This is a one-time cutover that requires all active sessions to be invalidated. Plan for users to re-authenticate after deployment.

Budget 2 to 4 weeks for a full Auth.js to Better Auth migration. If you are already considering this move, our managed auth provider comparison covers when it makes sense to go managed instead.

Migrating from Auth.js v4 to Auth.js v5

If you are staying in the Auth.js ecosystem, the v4 to v5 migration is necessary but painful. The API surface changed significantly: configuration moved from a route handler to a top-level auth() function, session access APIs changed, middleware behavior changed, and adapter interfaces were updated.

The official migration guide covers the API changes, but the real challenge is testing. Every protected route, every session check, every callback needs verification. We typically budget 1 to 2 weeks for v4 to v5 migrations on medium-sized apps.

Running Two Libraries Side by Side

For large applications, a gradual migration is sometimes the safest approach. You can run Auth.js and Better Auth simultaneously by mounting them on different route prefixes (/api/auth for the existing library, /api/auth-v2 for the new one). New features use the new library while existing flows continue to work. Once everything is migrated, you remove the old library.

This dual-stack approach adds complexity but eliminates the risk of a big-bang migration breaking authentication for all users at once. We have used it successfully on apps with 50K+ active users where downtime was not acceptable.

Our Recommendations: What to Pick for Your Next Project

After building production auth with all three libraries across dozens of projects, here is our honest take. These recommendations are specific and opinionated because vague "it depends" advice does not help you make a decision.

For New Projects in 2026: Use Better Auth

Better Auth is the right default for new Next.js projects that want self-hosted auth. The plugin architecture means you start lean and add features as your product demands them. The TypeScript experience is the best in the ecosystem. Database integration with Prisma and Drizzle is seamless. And the community momentum is undeniable: Better Auth's GitHub stars grew 4x in 2025, and the plugin ecosystem is expanding rapidly.

Better Auth is particularly strong for B2B SaaS where you need organizations, roles, and eventually SSO. These features are first-class plugins rather than custom code you have to maintain. Starting with Better Auth means you will not need to migrate when your first enterprise customer asks for team management.

The tradeoff: Better Auth is younger than Auth.js. The community is smaller, Stack Overflow answers are fewer, and some edge cases in the plugin system are still being ironed out. You will occasionally read GitHub issues instead of polished documentation. For experienced teams, this is manageable. For junior-heavy teams, the learning curve is steeper.

For Simple OAuth Apps: Use Auth.js

If your authentication needs are straightforward (Google login, GitHub login, maybe magic links, no email/password) then Auth.js is still a solid choice. Its OAuth provider ecosystem is unmatched, the configuration is minimal, and the library has years of battle-testing behind it. You can have OAuth working in 15 minutes with zero custom code.

Auth.js also makes sense if your team is already deeply familiar with NextAuth patterns and your product does not need features outside Auth.js's sweet spot. The cost of learning a new library is real, and sometimes the known quantity is the right business decision.

Do not choose Auth.js if you need email/password with database sessions, 2FA, organization management, or extensive session customization. These are not Auth.js's strengths, and fighting the library to add them is worse than choosing a library that supports them natively.

For Existing Lucia Projects: Plan Your Migration

If you are running Lucia in production today, you are not in immediate danger. The library receives security patches, and your auth works. But maintenance mode means no new features, and the ecosystem of community plugins and guides will slowly atrophy. Start planning your migration now while it is a proactive choice rather than an emergency.

Better Auth is the natural migration target. The philosophy is similar (self-hosted, database-first, TypeScript-native) but with significantly more built-in functionality. Most Lucia projects we have migrated saw a 40 to 60 percent reduction in auth-related code after moving to Better Auth, simply because plugins replaced custom implementations.

When to Skip Libraries Entirely and Go Managed

Self-hosted auth libraries are not always the right answer. If your team is small (1 to 3 developers), your product is early stage, and you just need auth to work so you can focus on your core product, a managed provider like Clerk gives you more leverage. You trade control and cost predictability for speed and reduced maintenance burden. If you are evaluating that path, we break down the tradeoffs in our managed auth provider comparison.

The self-hosted vs managed decision is less about technical capability and more about where your team's time creates the most value. If auth is a differentiator for your product, own it with Better Auth. If auth is a commodity that should be invisible, pay for Clerk and move on.

Need Help Making the Call?

Authentication architecture decisions ripple through your entire stack. Session management affects your API design. Database schema choices affect your query performance. Plugin selection affects your feature roadmap. Getting it right from the start saves weeks of engineering time and avoids painful migrations later.

We help startup teams evaluate auth libraries, design session architectures, and ship production-ready authentication as part of our full-stack Next.js engagements. Book a free strategy call and we will map your requirements to the right approach in 30 minutes.

Need help building this?

Our team has launched 50+ products for startups and ambitious brands. Let's talk about your project.

Better AuthNextAuth comparisonLucia authNext.js authenticationself-hosted auth library

Ready to build your product?

Book a free 15-minute strategy call. No pitch, just clarity on your next steps.

Get Started