writing/blog/2026/05
BlogMay 6, 2026·6 min read

Better Auth: TypeScript Authentication Done Right in 2026

Better Auth is the TypeScript authentication library replacing NextAuth in 2026. Self-hosted, type-safe, plugin-driven — here is why teams are migrating.

If you have shipped a Next.js or full-stack TypeScript app in the last few years, you have probably wrestled with authentication. NextAuth (now Auth.js) has been the de-facto choice since 2020, but anyone who has migrated between major versions, debugged a callback URL, or tried to add organizations and 2FA knows how painful it can get. Clerk solves the developer experience but locks you into a paid SaaS. Supabase Auth ties you to Supabase. Auth0 is enterprise-priced.

In 2026, a new option has taken over: Better Auth. It is open source, self-hosted, fully type-safe, framework-agnostic, and architected so the parts you do not need stay out of your bundle. This guide explains what it is, why it has caught on, and how to ship it in production.

The 30-second answer

Better Auth is a TypeScript-first authentication library that gives you NextAuth-style self-hosting with Clerk-style developer experience. It supports email/password, OAuth, magic links, passkeys, two-factor authentication, organizations, and admin features through a clean plugin system. You own the database, you own the sessions, and the types flow end-to-end from server to client without manual generation steps.

Pick Better Auth if you want full control of your auth stack without writing it from scratch, and you are tired of fighting your auth library on every upgrade.

Why teams are migrating away from NextAuth

The complaints are remarkably consistent across teams that switch:

  • Configuration sprawl. Auth.js v5 split configuration across multiple files and runtime contexts. A simple OAuth setup easily spans four files before it works.
  • Type safety leaks. Session types require module augmentation, and the generated types do not reflect custom fields without manual work.
  • Edge runtime friction. Middleware that talks to a database needs separate config. The split between edge and Node runtimes is leaky.
  • Plugins are second-class. Adding 2FA or organizations means writing it yourself or duct-taping community packages.
  • Migrations are risky. Major version bumps have broken sessions, redirect flows, and provider configs more than once.

Better Auth was designed in direct response to these issues. The thesis: auth is core infrastructure, so the library should be a library — not a framework with opinions about your file layout.

Core architecture

Better Auth runs as a single set of API routes mounted under /api/auth/*. You configure it once in a server file, and the same config powers your server actions, route handlers, and the client SDK. Everything is plain TypeScript — no DSL, no codegen step, no virtual modules.

A minimal Next.js setup looks like this:

import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { db } from "@/db"
 
export const auth = betterAuth({
  database: drizzleAdapter(db, { provider: "pg" }),
  emailAndPassword: { enabled: true },
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    },
  },
})

That single object is the source of truth. The handler is mounted in one place:

import { auth } from "@/lib/auth"
import { toNextJsHandler } from "better-auth/next-js"
 
export const { GET, POST } = toNextJsHandler(auth)

And the client SDK is a one-liner:

import { createAuthClient } from "better-auth/react"
 
export const authClient = createAuthClient()

The client infers its types directly from the server config. Add a plugin on the server, and the matching methods appear on the client. No regeneration step, no manual type augmentation.

Database adapters and ownership

Better Auth ships first-party adapters for Drizzle, Prisma, Kysely, and MongoDB. The schema is generated by the CLI and lives in your codebase like any other migration. Sessions, accounts, and user records are stored in your database — there is no external service holding state.

Generating the schema:

npx @better-auth/cli generate
npx @better-auth/cli migrate

The generated tables follow conventional names: user, session, account, verification. You can extend the user table with arbitrary fields through the config, and those fields stay typed end-to-end:

export const auth = betterAuth({
  user: {
    additionalFields: {
      role: { type: "string", defaultValue: "member" },
      companyId: { type: "string", required: false },
    },
  },
})

session.user.role is now typed as string everywhere — server, client, middleware. No manual augmentation file required.

The plugin system is the headline feature

The plugin system is where Better Auth pulls ahead of every alternative. Each capability ships as a plugin you opt into. The bundle only carries what you enable.

import { twoFactor, organization, admin, passkey, magicLink } from "better-auth/plugins"
 
export const auth = betterAuth({
  plugins: [
    twoFactor(),
    organization({ allowUserToCreateOrganization: true }),
    admin(),
    passkey({ rpName: "Noqta" }),
    magicLink({ sendMagicLink: async ({ email, url }) => sendEmail(email, url) }),
  ],
})

Each plugin adds its own routes, database tables, and client methods. The client SDK auto-discovers them:

await authClient.twoFactor.enable({ password: "..." })
await authClient.organization.create({ name: "Noqta" })
await authClient.passkey.addPasskey()
await authClient.signIn.magicLink({ email: "user@example.com" })

Available official plugins in 2026 cover two-factor authentication, magic links, passkeys (WebAuthn), organizations and teams, admin tooling, API keys, anonymous sessions, multi-session, OIDC provider mode, and rate limiting. Community plugins extend this further with SAML, SSO, and custom flows.

Sessions and middleware

Sessions are JWT-backed by default but can be switched to database sessions with one config flag. Either way, reading the session in any context is uniform:

import { auth } from "@/lib/auth"
import { headers } from "next/headers"
 
const session = await auth.api.getSession({ headers: await headers() })
if (!session) redirect("/login")

The same call works in Server Components, Route Handlers, Server Actions, and middleware. Middleware does not need a separate edge-compatible config — Better Auth provides a lightweight session check that runs on the edge without database access:

import { getSessionCookie } from "better-auth/cookies"
import { NextResponse } from "next/server"
 
export async function middleware(request: Request) {
  const sessionCookie = getSessionCookie(request)
  if (!sessionCookie) return NextResponse.redirect(new URL("/login", request.url))
  return NextResponse.next()
}

For full session validation in middleware you can call the API, but the cookie check is enough for most route protection and avoids a database round-trip on every request.

Type safety end-to-end

This is the part that genuinely surprises people coming from NextAuth. You add a field to the user model in config, and three things happen automatically:

  1. The database schema migration adds the column.
  2. The server session.user type includes it.
  3. The client authClient.useSession() hook returns it with the correct type.

No next-auth.d.ts augmentation, no client-server type drift, no as any. Plugins extend the types the same way — enable the organization plugin and session.activeOrganization appears as a typed field on both sides.

Performance and bundle size

Better Auth is built around tree-shaking. The base library without plugins adds roughly 25 KB gzipped to the client bundle. Each plugin you enable adds its own client code, but plugins you do not enable contribute nothing. NextAuth v5 by comparison ships closer to 50 KB and pulls in providers you may not use.

On the server, request handling is a small dispatch over the configured routes. No reflection, no module loading at request time. Cold starts on Vercel and Cloudflare Workers are noticeably faster than NextAuth.

Where Better Auth still has rough edges

It is not perfect. A few areas worth knowing about before adopting:

  • Documentation is improving but uneven. Plugin docs are sometimes thin, and you occasionally need to read the source to understand a flag.
  • Community plugins vary in quality. Stick to official plugins for production until a community plugin has a track record.
  • Enterprise SSO (SAML) is community-maintained. If you need SAML for B2B sales, evaluate the SAML plugin carefully or pair Better Auth with a dedicated SSO provider.
  • Migrating from NextAuth is not automatic. Sessions, password hashes, and account links all need to be moved deliberately. Plan for a one-time migration window.

Migration pattern from NextAuth

The cleanest migration we have run looks like this:

  1. Stand up Better Auth alongside NextAuth in the same app under a different prefix (for example /api/auth-v2/*).
  2. Migrate the user, account, and session tables. Better Auth's schema is similar enough that a single SQL migration covers most fields.
  3. For password-based users, keep the old hash column and use Better Auth's verifyPassword hook to validate the legacy hash on first login, then rehash to the new format.
  4. For OAuth users, the account links carry over with a column rename. No re-authentication required.
  5. Switch the client SDK over feature by feature behind a flag. Run both in parallel for a week to catch edge cases.
  6. Decommission NextAuth once metrics are stable.

We ran this migration on a production Next.js app in March 2026. Total downtime was zero, the bug count for the first week was three, and the team has not looked back.

When not to use Better Auth

A few cases where it is the wrong call:

  • You need a hosted UI today and have no engineering bandwidth. Clerk gives you a polished prebuilt component, hosted user management, and SOC 2 compliance out of the box. Better Auth gives you primitives.
  • You are deeply embedded in Supabase. Supabase Auth is fine, integrates with RLS, and there is no migration upside if Supabase is already your stack.
  • You need SAML SSO with vendor support contracts. Auth0 and WorkOS exist for a reason.

For everyone else — most independent SaaS teams and most Next.js applications — Better Auth is now the default we recommend at Noqta when starting a new project.

Getting started

Install and scaffold:

npm install better-auth
npx @better-auth/cli init

The CLI walks you through database adapter selection, generates the schema, creates the auth config file, and sets up the client. Within fifteen minutes you can have email/password, Google OAuth, and a working session in a fresh Next.js app.

Documentation lives at the official site, and the source is on GitHub under an MIT licence. The library is on a steady release cadence with a clear roadmap and the kind of issue tracker that gets responses in days, not weeks.

Authentication used to be the part of every project we dreaded touching. With Better Auth, it has become the part we set up first and forget about.