better-auth — The Open-Source Auth Library That Replaces NextAuth

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Auth.js v5 (NextAuth) carries years of historical baggage that makes it awkward to configure in modern TypeScript projects. better-auth is a cleaner alternative: TypeScript-first, framework-agnostic, and designed specifically for applications that need multi-tenancy, passkeys, and granular session control from day one.

What Makes better-auth Different

better-auth is a self-hosted TypeScript auth library built around a plugin model. It handles session management, user registration, OAuth, and 2FA without coupling you to any particular framework or ORM. The same auth config object works with Express, Fastify, Hono, and Next.js.

The core setup is explicit and minimal:

import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { twoFactorPlugin } from 'better-auth/plugins/two-factor';
import { organizationPlugin } from 'better-auth/plugins/organization';
import { db } from './db';
 
export const auth = betterAuth({
  database: drizzleAdapter(db),
  secret: process.env.BETTER_AUTH_SECRET,
  plugins: [
    twoFactorPlugin({ issuer: 'My App' }),
    organizationPlugin({ roles: ['member', 'admin', 'owner'] }),
  ],
});

No magic config files. Every behavior is explicit and auditable.

Framework Integration

better-auth abstracts the request/response cycle so the same auth instance works everywhere.

Express:

import express from 'express';
import { auth } from './auth';
 
const app = express();
app.use(express.json());
 
app.all('/api/auth/*', (req, res) => auth.handler(req, res));
 
app.get('/api/me', async (req, res) => {
  const session = await auth.api.getSession({ headers: req.headers });
  if (!session) return res.status(401).json({ error: 'Unauthorized' });
  res.json(session);
});

Hono:

import { Hono } from 'hono';
import { auth } from './auth';
 
const app = new Hono();
app.all('/api/auth/*', (c) => auth.handler(c.req.raw));
 
export default app;

Next.js App Router:

// app/api/auth/[...nextauth]/route.js
import { auth } from '@/auth';
export const { GET, POST } = auth.handler();

The same auth object works in all three. No framework-specific rewrites when you migrate.

Built-in Plugins

Two-Factor Authentication (TOTP):

import { twoFactorPlugin } from 'better-auth/plugins/two-factor';
 
// Server setup
export const auth = betterAuth({
  plugins: [twoFactorPlugin({ issuer: 'My App' })],
});
 
// Client: enable 2FA
const { verificationCode } = await client.twoFactor.enable({
  password: 'user_password',
});
// Show QR code, verify with authenticator
await client.twoFactor.verifyCode({ code: '123456' });

Passkeys (WebAuthn):

import { passkeyPlugin } from 'better-auth/plugins/passkey';
 
export const auth = betterAuth({
  plugins: [passkeyPlugin()],
});
 
// Client: register device
await client.passkey.register({ name: 'MacBook Pro' });
 
// Client: sign in
const { user, session } = await client.passkey.authenticate();

Magic Link:

import { magicLinkPlugin } from 'better-auth/plugins/magic-link';
 
export const auth = betterAuth({
  plugins: [
    magicLinkPlugin({
      sendEmail: async (email, code) => {
        await sendEmail({
          to: email,
          subject: 'Your magic link',
          text: `Sign in: https://app.example.com/verify?code=${code}`,
        });
      },
    }),
  ],
});

OAuth (GitHub, Google):

export const auth = betterAuth({
  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,
    },
  },
});

Database Adapters

better-auth ships first-class adapters for Drizzle, Prisma, and MongoDB. Schemas are created automatically.

// Drizzle
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
export const auth = betterAuth({ database: drizzleAdapter(db) });
 
// Prisma
import { prismaAdapter } from 'better-auth/adapters/prisma';
export const auth = betterAuth({ database: prismaAdapter(prisma) });

Pick the ORM your project already uses — no migration required.

Organisation Plugin for Multi-Tenancy

The organisation plugin adds workspace-level isolation with per-member roles:

export const auth = betterAuth({
  plugins: [
    organizationPlugin({ roles: ['member', 'admin', 'owner'] }),
  ],
});
 
// In an API route
async function protectedRoute(req) {
  const { data } = await auth.api.getSession({ headers: req.headers });
 
  if (!data?.session) return { status: 401 };
 
  const role = data.session.user.organizationRole;
  if (role !== 'admin') return { status: 403 };
 
  // Proceed with admin action
}

Users join organisations, roles gate access. No custom RBAC tables needed.

Custom Session Claims and Rate Limiting

Embed plan and feature-flag data directly into the session JWT to avoid extra database queries per request:

export const auth = betterAuth({
  callbacks: {
    async jwt(data) {
      return {
        ...data.jwt,
        plan: 'premium',
        features: ['analytics', 'api'],
      };
    },
  },
});
 
// Access in a route handler
const session = await auth.api.getSession({ headers });
console.log(session.user.plan); // 'premium'

For rate limiting, add the built-in plugin:

import { rateLimitPlugin } from 'better-auth/plugins/rate-limit';
 
export const auth = betterAuth({
  plugins: [
    rateLimitPlugin({ enabled: true, window: 60000, max: 10 }),
  ],
});

Migrating from Auth.js (NextAuth)

A safe migration runs both systems in parallel:

  1. Install better-auth and point a new route prefix at it (/api/auth-new/*)
  2. Migrate the client from next-auth/react to @better-auth/react — the useSession API is nearly identical
  3. Update database queries to use better-auth user ID format
  4. Cut over the route prefix once tests pass

Most NextAuth-to-better-auth migrations take one to two sprints depending on the number of OAuth providers and how deeply session data is embedded in application code.

better-auth vs Auth.js v5

Featurebetter-authAuth.js v5
LanguageTypeScript-firstTypeScript supported
Plugin systemBuilt-in, cleanOptional, scattered
PasskeysBuilt-in pluginLimited
Organisation/multi-tenancyBuilt-in pluginNot built-in
Maturity2024+Battle-tested

Auth.js v5 is the right choice for projects already using it at scale. For greenfield projects, better-auth's explicit plugin model and cleaner TypeScript experience make it the stronger default in 2026.

Key Takeaways

  • better-auth is a TypeScript-first, framework-agnostic auth library that works identically in Express, Fastify, Hono, and Next.js
  • Plugins for 2FA, passkeys, magic links, and organisations are first-class and maintained in the same repository
  • Database adapters for Drizzle, Prisma, and MongoDB are included; schemas are auto-created
  • The organisation plugin provides workspace-level isolation and per-member roles without custom RBAC tables
  • Session JWT callbacks let you embed plan and feature flags, eliminating extra database queries on each request
  • The rate limiting plugin runs at the auth layer, protecting login and signup endpoints without additional middleware
  • Migrating from Auth.js is safest by running both systems on parallel route prefixes before cutting over
  • For new projects in 2026, better-auth's plugin model, cleaner API, and native passkey support make it the recommended default over Auth.js v5

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading