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

- Name
- Sanjeev Sharma
- @webcoderspeed1
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
- Setup with Express, Fastify, Hono, Next.js
- Built-in Plugins
- Database Adapters
- Organisation/Team Plugin for Multi-Tenancy
- Admin Plugin
- Rate Limiting Plugin
- Custom Session Data
- Comparing to Auth.js v5
- Migrating from NextAuth to better-auth
- Checklist
- Conclusion
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
| Feature | better-auth | Auth.js v5 |
|---|---|---|
| Language | TypeScript-first | TypeScript support |
| Setup complexity | Simple | Moderate |
| Plugin system | Built-in, clean | Optional, scattered |
| Database agnostic | Yes | Yes |
| Self-hostable | Yes | Yes |
| Maturity | Growing | Stable |
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.