Published on

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

Authors

Introduction

NextAuth (now Auth.js v5) is bloated. It carries historical baggage, optional plugins, and configuration that feels fragile. better-auth is the cleaner alternative: TypeScript-first, framework-agnostic, and built for modern applications from the ground up.

This post explores better-auth's architecture, compares it to Auth.js v5, shows how to add multi-tenancy with the organisations plugin, and walks through migration patterns if you're leaving NextAuth.

What better-auth Is

better-auth is a TypeScript library that provides session management, user registration, OAuth, and plugin architecture—without the framework coupling. It works with Express, Fastify, Hono, Next.js, or any Node.js runtime.

import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { db } from './db';

export const auth = new BetterAuth({
  database: drizzleAdapter(db),
  secret: process.env.BETTER_AUTH_SECRET,
  plugins: [
    twoFactorPlugin(),
    passkeyPlugin(),
    organizationPlugin(),
  ],
});

It's minimal, explicit, and type-safe. No magic config files.

Setup with Express, Fastify, Hono, Next.js

Express:

import express from 'express';
import { auth } from './auth';

const app = express();
app.use(express.json());

// Mount better-auth routes
app.all('/api/auth/*', (req, res) => auth.handler(req, res));

// Protected route
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);
});

app.listen(3000);

Fastify:

import fastify from 'fastify';
import { auth } from './auth';

const app = fastify();

app.all('/api/auth/*', async (req, reply) => {
  return auth.handler(req, reply);
});

await app.listen({ port: 3000 });

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/api/auth/[...nextauth]/route.ts
import { auth } from '@/auth';

export const { GET, POST } = auth.handler();

better-auth abstracts the framework. Use the same auth config everywhere.

Built-in Plugins

Two-Factor Authentication:

import { twoFactorPlugin } from 'better-auth/plugins/two-factor';

export const auth = new BetterAuth({
  plugins: [
    twoFactorPlugin({
      issuer: 'My App',
    }),
  ],
});

// Client-side
const { verificationCode } = await client.twoFactor.enable({
  password: 'user_password',
});
// Display QR code, user scans with authenticator app

await client.twoFactor.verifyCode({ code: '123456' });

Passkeys:

import { passkeyPlugin } from 'better-auth/plugins/passkey';

export const auth = new BetterAuth({
  plugins: [passkeyPlugin()],
});

// Client registers passkey
const credential = await client.passkey.register({
  name: 'MacBook Pro',
});

// Client authenticates with passkey
const { user, session } = await client.passkey.authenticate();

Magic Link:

import { magicLinkPlugin } from 'better-auth/plugins/magic-link';

export const auth = new BetterAuth({
  plugins: [
    magicLinkPlugin({
      sendEmail: async (email, code) => {
        await sendEmail({
          to: email,
          subject: 'Your magic link',
          text: `Click: https://app.example.com/verify?code=${code}`,
        });
      },
    }),
  ],
});

// Sign in flow
await client.magicLink.sendSignInEmail({ email: 'user@example.com' });

// Verify link
await client.magicLink.verifyEmail({ code: 'abc123' });

OAuth:

export const auth = new 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!,
    },
  },
});

All plugins integrate cleanly. No feature detection or conditional imports.

Database Adapters

Drizzle:

import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { db } from './db';

export const auth = new BetterAuth({
  database: drizzleAdapter(db),
});

Prisma:

import { prismaAdapter } from 'better-auth/adapters/prisma';
import { prisma } from './db';

export const auth = new BetterAuth({
  database: prismaAdapter(prisma),
});

MongoDB:

import { mongoAdapter } from 'better-auth/adapters/mongodb';

export const auth = new BetterAuth({
  database: mongoAdapter(mongoClient.db('myapp')),
});

Adapters handle schema creation automatically. Use whatever ORM you prefer.

Organisation/Team Plugin for Multi-Tenancy

import { organizationPlugin } from 'better-auth/plugins/organization';

export const auth = new BetterAuth({
  plugins: [
    organizationPlugin({
      roles: ['member', 'admin', 'owner'],
    }),
  ],
});

// API route
const { data, error } = await auth.api.getSession({
  headers: req.headers,
});

if (!data?.session) return res.status(401).json({});

const orgId = data.session.user.organizationId;
const role = data.session.user.organizationRole;

if (role !== 'admin') return res.status(403).json({});

// Safe to proceed

Organisations are isolated. Users join orgs, roles control access.

Admin Plugin

import { adminPlugin } from 'better-auth/plugins/admin';

export const auth = new BetterAuth({
  plugins: [adminPlugin()],
});

// Only for admins
const users = await auth.admin.listUsers();
await auth.admin.updateUser({ id: 'user_id', email: 'new@example.com' });
await auth.admin.deleteUser({ id: 'user_id' });

The admin plugin requires a separate admin role. Prevent accidental exposure.

Rate Limiting Plugin

import { rateLimitPlugin } from 'better-auth/plugins/rate-limit';

export const auth = new BetterAuth({
  plugins: [
    rateLimitPlugin({
      enabled: true,
      window: 60 * 1000, // 1 minute
      max: 10, // 10 requests per window
    }),
  ],
});

Prevents brute force attacks. Configurable per endpoint.

Custom Session Data

export const auth = new BetterAuth({
  callbacks: {
    async jwt(data) {
      // Add custom claims to JWT
      return {
        ...data.jwt,
        plan: 'premium',
        features: ['analytics', 'api'],
      };
    },
  },
});

// Access in routes
const session = await auth.api.getSession({ headers });
console.log(session.user.plan); // 'premium'

Store role, plan, and feature flags in the JWT. Avoid database queries.

Comparing to Auth.js v5

Featurebetter-authAuth.js v5
LanguageTypeScript-firstTypeScript support
Setup complexitySimpleModerate
Plugin systemBuilt-in, cleanOptional, scattered
Database agnosticYesYes
Self-hostableYesYes
MaturityGrowingStable

Auth.js v5 is battle-tested in production at scale. better-auth is newer, faster to configure, and cleaner architecture. For new projects, better-auth. For existing Auth.js, migration is worth it only if you're redesigning auth anyway.

Migrating from NextAuth to better-auth

Step 1: Install better-auth and configure with existing database

import { drizzleAdapter } from 'better-auth/adapters/drizzle';

export const auth = new BetterAuth({
  database: drizzleAdapter(db),
  secret: process.env.AUTH_SECRET,
});

Step 2: Point a new route to better-auth

// New route handlers
app.all('/api/auth-new/*', (req, res) => auth.handler(req, res));

Step 3: Migrate client code

// Old NextAuth
import { useSession } from 'next-auth/react';
const { data: session } = useSession();

// New better-auth
import { useSession } from '@better-auth/react';
const { data: session } = useSession();

Step 4: Update database queries to use better-auth's user ID format

Most NextAuth to better-auth migrations take 1-2 sprints. Run both in parallel during transition.

Checklist

  • Install better-auth and database adapter
  • Configure OAuth providers
  • Add organisation plugin if needed
  • Implement session sync to your database
  • Test signup, login, and logout flows
  • Configure rate limiting
  • Set up password reset email delivery
  • Add 2FA or passkey plugin
  • Write API middleware for session validation
  • Plan migration path from existing auth

Conclusion

better-auth is the modern auth library for Node.js. It's cleaner than Auth.js, framework-agnostic, and production-ready. Its plugin system is extensible, documentation is clear, and TypeScript support is native. If you're evaluating auth libraries in 2026, better-auth deserves serious consideration over the legacy options.