| name | better-auth |
| description | Better Auth: the open-source auth framework for Next.js/TypeScript — session management, OAuth, 2FA, RBAC, Drizzle/Prisma adapters, no vendor lock-in |
Better Auth Skill
When to activate
- Setting up authentication in a Next.js or TypeScript project from scratch
- Adding OAuth providers (Google, GitHub, etc.) to an existing app
- Implementing 2FA, TOTP, or magic link authentication
- Setting up role-based access control (RBAC) or organization/team auth
- Migrating away from Clerk, Auth0, or NextAuth due to cost or lock-in
- Integrating auth with Drizzle ORM or Prisma
When NOT to use
- Projects already on NextAuth v5/Auth.js with working auth — migration cost is high
- When you only need a simple JWT token and nothing else — overkill
- Non-TypeScript projects — Better Auth is TypeScript-first
Why Better Auth for AI generation
Auth is the #1 area where LLMs hallucinate dangerously — incorrect cookie settings, missing CSRF headers, broken OAuth redirect flows. Better Auth's modular plugin system means Claude can inject pre-tested configuration blocks for 2FA, RBAC, and OAuth without generating cryptographic logic from scratch. The research confirms: "a single logic flaw results in catastrophic data breaches."
Instructions
Installation
npm install better-auth
Database setup (Drizzle)
import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core'
export const user = pgTable('user', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').notNull(),
image: text('image'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
})
export const session = pgTable('session', {
id: text('id').primaryKey(),
expiresAt: timestamp('expires_at').notNull(),
token: text('token').notNull().unique(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
})
export const account = pgTable('account', {
id: text('id').primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at'),
scope: text('scope'),
password: text('password'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
})
export const verification = pgTable('verification', {
id: text('id').primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at'),
updatedAt: timestamp('updated_at'),
})
Auth configuration (server)
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { twoFactor, organization, admin } from 'better-auth/plugins'
import { db } from '@/db'
import * as schema from '@/db/auth-schema'
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
schema,
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
minPasswordLength: 8,
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
plugins: [
twoFactor(),
(),
(),
],
: {
: * * * ,
: * * ,
: {
: ,
: * ,
},
},
: [process..!],
})
= auth..
= auth...
Route handler (Next.js App Router)
import { auth } from '@/lib/auth'
import { toNextJsHandler } from 'better-auth/next-js'
export const { POST, GET } = toNextJsHandler(auth)
Client setup
import { createAuthClient } from 'better-auth/react'
import { twoFactorClient, organizationClient, adminClient } from 'better-auth/client/plugins'
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
plugins: [
twoFactorClient(),
organizationClient(),
adminClient(),
],
})
export const {
signIn,
signUp,
signOut,
useSession,
getSession,
} = authClient
Usage in components
'use client'
import { authClient } from '@/lib/auth-client'
await authClient.signIn.email({ email, password })
await authClient.signIn.social({ provider: 'google' })
await authClient.signUp.email({ email, password, name })
await authClient.signOut()
function ProfileButton() {
const { data: session, isPending } = authClient.useSession()
if (isPending) return <Spinner />
if (!session) return <Link href="/login">Sign in</Link>
return <span>{session.user.name}</span>
}
Server-side session access
import { auth } from '@/lib/auth'
import { headers } from 'next/headers'
export default async function DashboardPage() {
const session = await auth.api.getSession({ headers: await headers() })
if (!session) redirect('/login')
return <div>Welcome {session.user.name}</div>
}
async function updateProfile(formData: FormData) {
'use server'
const session = await auth.api.getSession({ headers: await headers() })
if (!session) throw new Error('Unauthorized')
}
Middleware — route protection
import { NextRequest, NextResponse } from 'next/server'
import { getSessionCookie } from 'better-auth/cookies'
export async function middleware(request: NextRequest) {
const sessionCookie = getSessionCookie(request)
if (!sessionCookie && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
}
Two-Factor Authentication (2FA)
await authClient.twoFactor.enable({ password: currentPassword })
const { data } = await authClient.twoFactor.getTotpUri({ password })
await authClient.twoFactor.verifyTotp({ code: '123456' })
const result = await authClient.signIn.email({ email, password })
if (result.data?.twoFactorRedirect) {
await authClient.twoFactor.verifyTotp({ code })
}
Organizations (multi-tenant)
await authClient.organization.create({ name: 'Acme Corp', slug: 'acme' })
await authClient.organization.inviteMember({
email: 'colleague@acme.com',
role: 'member',
organizationId: org.id,
})
const { data: activeOrg } = await authClient.organization.getActiveMember()
const session = await auth.api.getSession({ headers: await headers() })
const orgId = session?.session.activeOrganizationId
CLI commands
npx better-auth generate
npx better-auth migrate
npx better-auth admin
Example
User: Add Better Auth to a Next.js + Drizzle + Neon project with Google OAuth, email/password, email verification, and protect /dashboard routes.
Expected output:
db/auth-schema.ts — generated user/session/account/verification tables
lib/auth.ts — betterAuth() config with drizzleAdapter, emailAndPassword, Google social provider, email verification
lib/auth-client.ts — createAuthClient() with base URL
app/api/auth/[...all]/route.ts — toNextJsHandler(auth)
middleware.ts — getSessionCookie check on /dashboard/:path*
app/login/page.tsx — sign-in form using authClient.signIn.email + Google button