mrkeyoor.com_
Sat 08 Aug 17:41 UTC
npmSecurityupdated 08 Aug 2026

better-auth

Better Auth is a self-hosted TypeScript authentication and authorization framework. Its server owns users, linked accounts, cookie sessions, email and password login, OAuth providers, schema generation, rate limiting, and framework-neutral Request and Response handlers. Typed clients cover React, Vue, Svelte, Solid, and vanilla code. Plugins add two-factor authentication, passkeys, magic links, organizations, teams, API keys, SSO, SCIM, admin tools, and more. Unlike a hosted identity service, your application runs the code, stores the data, sends the mail, configures providers, and responds to incidents.

Verdict

The most complete self-hosted TypeScript auth framework for teams that genuinely want to own identity data and operations. Do not choose it merely to avoid a vendor bill; the schema, mail, proxy, runtime, recovery, monitoring, and incident workload is real.

API stability3/5The 1.x release has a coherent betterAuth server, createAuthClient clients, auth.api methods, adapters, and plugin pattern, but the surface is expanding quickly across dozens of plugins and runtime integrations. Current docs already call out Next.js 16 proxy changes, Express 5 route syntax, Prisma 7 output paths, experimental joins, and a future Cloudflare AsyncLocalStorage assumption. Pin exact versions and treat plugin changes like application API changes.
Docs5/5The documentation is unusually broad and candid for an auth framework. It covers secrets, base URLs, every database and ORM path, CLI limits, core schemas, cookies, Safari ITP, trusted proxies, IPv6 rate limiting, session freshness and caching, enumeration resistance, framework handlers, provider options, server-side cookie propagation, plugin schemas, and detailed security gotchas. The amount is daunting, but auth deserves that depth.
Maintenance5/5Version 1.6.26 was published August 4, 2026 and the repository was pushed August 7, 2026. The non-archived repository has 29,492 stars and GitHub reports 654 open issues and pull requests, a large queue that matches its scope and adoption. Releases, adapters, docs, framework guides, security reporting, end-to-end tests, and plugin work are visibly active, though the pace increases upgrade and regression risk.
Ecosystem5/5The npm download API reports 6,485,638 downloads in the latest week. Official integrations span major TypeScript web frameworks and runtimes, built-in and ORM adapters cover common SQL databases plus MongoDB, and the plugin catalog includes 2FA, organizations, passkeys, SSO, SCIM, API keys, OAuth and OIDC providers, admin features, and payment integrations. The ecosystem is young but already broad enough to replace several packages.

Use it if

  • You want authentication data in your own SQL or MongoDB database and are prepared to operate the complete security boundary
  • A TypeScript application needs email and password, social login, sessions, and advanced features behind one typed server and client API
  • The same auth core must serve Next.js, Nuxt, SvelteKit, Hono, Express, TanStack Start, Cloudflare Workers, or another Fetch-compatible framework
  • Organizations, teams, passkeys, 2FA, API keys, SSO, or other plugins would otherwise require several unrelated packages and schemas
Skip it if

Setup reality

The package install is the smallest step. Generate a high-entropy BETTER_AUTH_SECRET of at least 32 characters, set the canonical BETTER_AUTH_URL, and configure trusted origins and proxy behavior for every deployed hostname. Secret rotation can use BETTER_AUTH_SECRETS, but it needs a planned rollout. Most deployments need a database plus the matching driver or adapter. Built-in Kysely paths support SQLite, PostgreSQL, and MySQL; Prisma, Drizzle, MongoDB, and other adapters add their own package, client lifecycle, runtime limits, and migration tooling. Better Auth's CLI can run migrate only for the built-in Kysely adapter. With Prisma or Drizzle, run npx auth@latest generate and then apply the result through that ORM. Prisma 7 custom client output changes the import path, and its adapter does not support CLI-driven migration. Every plugin can change the schema, so regenerate before deploying code that enables it. Mount a GET and POST catch-all handler at /api/auth/* and create the matching framework client. Different client and server origins need the full base URL, CORS, trustedOrigins, and cookie settings. OAuth requires provider applications, exact callback URLs, client secrets, and production redirect registration. Email verification, password reset, magic links, and email OTP require your mail transport, templates, delivery retries, and anti-enumeration choices. The docs advise not awaiting verification or reset email directly because timing can reveal account state; serverless environments need waitUntil or an equivalent so background sending is not terminated. Default production rate limiting is process memory, which does not coordinate across serverless instances or replicas. Put counters in the database or secondary storage and configure a trusted client-IP header or explicit trusted proxies; auth.api server calls bypass the built-in limiter. Sessions default to database-backed cookies and refresh behavior, so replicas, cookie caches, cross-subdomain setups, Safari ITP, secure-cookie flags, reverse proxies, and revocation latency all need tests. Plugins must be registered on the server and, when they extend client methods, on the client too. The package offers no finished sign-in UI. At roughly 173 KB gzipped for the main server entry, use better-auth/minimal with an external database adapter when the documented tradeoff fits. Authentication is security-critical, so pin versions, read changelogs, test migrations on a copy, and exercise login, logout, reset, verification, session revocation, OAuth state, CSRF, and rate limits before each upgrade.

Patterns

Configure PostgreSQL and email-password authconfigure-auth-server

import { Pool } from 'pg'
import { betterAuth } from 'better-auth'

export const auth = betterAuth({
  database: new Pool({ connectionString: process.env.DATABASE_URL }),
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
  },
  trustedOrigins: ['https://app.example.com'],
})

Set BETTER_AUTH_SECRET to at least 32 high-entropy characters and BETTER_AUTH_URL to the canonical public origin. Install pg separately.

Generate and apply the auth schemagenerate-database-schema

npx auth@latest generate --yes
# Review the generated Prisma, Drizzle, or SQL schema.
# Apply it with your ORM migration command.

# Built-in Kysely adapter only:
npx auth@latest migrate --yes

The Better Auth migrate command is only for its built-in Kysely adapter. Prisma, Drizzle, and other adapters must apply generated changes with their own migration tools.

Mount the Next.js App Router endpointmount-nextjs-handler

// app/api/auth/[...all]/route.ts
import { auth } from '@/lib/auth'
import { toNextJsHandler } from 'better-auth/next-js'

export const { GET, POST } = toNextJsHandler(auth)

Keep the catch-all at /api/auth unless basePath and the client baseURL are changed together. Pages Router uses toNodeHandler and must disable body parsing.

Create the React clientcreate-react-auth-client

// lib/auth-client.ts
import { createAuthClient } from 'better-auth/react'

export const authClient = createAuthClient({
  baseURL: 'https://app.example.com',
})

baseURL can be omitted for same-origin deployments. Cross-origin clients also need server trustedOrigins, CORS, and cookie settings.

Create an email-password accountsign-up-with-email

const { data, error } = await authClient.signUp.email({
  name: 'Ada Lovelace',
  email: 'ada@example.com',
  password,
  callbackURL: '/dashboard',
})

if (error) showError(error.message)

The default minimum password length is eight. With the default autoSignIn behavior, existing-email signup can return 422 and reveal registration unless verification or autoSignIn settings enable enumeration protection.

Sign in and detect a second-factor challengesign-in-and-handle-2fa

await authClient.signIn.email(
  { email, password, rememberMe: true },
  {
    onSuccess(ctx) {
      if (ctx.data.twoFactorRedirect) {
        showTwoFactor(ctx.data.twoFactorMethods)
      } else {
        goToDashboard()
      }
    },
    onError(ctx) {
      showError(ctx.error.message)
    },
  },
)

A 2FA challenge has no authenticated session yet. Server hooks must null-check newSession, and default 2FA enforcement covers credential sign-ins rather than OAuth or passkeys.

Start a GitHub OAuth flowsign-in-with-oauth

await authClient.signIn.social({
  provider: 'github',
  callbackURL: '/dashboard',
  errorCallbackURL: '/sign-in?oauth=failed',
  newUserCallbackURL: '/welcome',
})

Configure the GitHub client ID and secret on the server and register the exact production callback URL with GitHub. Validate every post-auth redirect destination.

Validate a session on the serverrequire-server-session

import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import { auth } from '@/lib/auth'

const session = await auth.api.getSession({
  headers: await headers(),
})
if (!session) redirect('/sign-in')
console.log(session.user.id)

A cookie-existence check is only an optimistic redirect hint, not authorization. Validate the session again inside every protected page, route, or action.

Read reactive session state in Reactrender-react-session

import { authClient } from '@/lib/auth-client'

export function AccountMenu() {
  const { data: session, isPending, error, refetch } = authClient.useSession()
  if (isPending) return <span>Loading...</span>
  if (error || !session) return <a href="/sign-in">Sign in</a>
  return <button onClick={() => authClient.signOut()}>{session.user.name}</button>
}

The React client uses a Nano Stores-backed reactive session. Treat pending, error, and signed-out states separately to avoid flashing protected content.

Send password-reset links and revoke sessionsconfigure-recovery-email

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    revokeSessionsOnPasswordReset: true,
    sendResetPassword: async ({ user, url }) => {
      void enqueueEmail({
        to: user.email,
        subject: 'Reset your password',
        text: `Open this link: ${url}`,
      })
    },
  },
})

The docs advise not awaiting mail delivery because response timing can reveal whether an account exists. Use a durable queue or serverless waitUntil so the send is not dropped.

Register TOTP and backup-code supportenable-two-factor-plugin

// server
import { twoFactor } from 'better-auth/plugins'
export const auth = betterAuth({
  appName: 'Acme Console',
  plugins: [twoFactor()],
})

// client
import { twoFactorClient } from 'better-auth/client/plugins'
export const authClient = createAuthClient({
  plugins: [twoFactorClient({ twoFactorPage: '/two-factor' })],
})

Generate and apply the plugin schema. Enabling returns a TOTP URI and backup codes, but twoFactorEnabled remains false until the user verifies a TOTP code.

Use database-backed limits behind a trusted proxypersist-rate-limits

export const auth = betterAuth({
  advanced: {
    ipAddress: {
      ipAddressHeaders: ['cf-connecting-ip'],
    },
  },
  rateLimit: {
    enabled: true,
    window: 60,
    max: 100,
    storage: 'database',
    customRules: {
      '/sign-in/email': { window: 10, max: 3 },
    },
  },
})

Create the rateLimit table through the proper migration flow. Only trust a header that your locked-down proxy overwrites; auth.api server calls do not pass through client-request rate limiting.

Alternatives

PackageRegistryPick it when
next-authnpmA Next.js application prefers Auth.js conventions, provider breadth, and its established framework-specific community
passportnpmAn Express-style server wants authentication strategies and will build its own sessions, user model, and account flows
supertokens-nodenpmYou want an open-source dedicated auth service with backend SDKs and prebuilt UI recipes rather than an in-process framework
iron-sessionnpmThe requirement is only an encrypted stateless cookie session, with no user database, OAuth, recovery, or organization model