---
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

```bash
npm install better-auth
```

### Database setup (Drizzle)

```typescript
// db/auth-schema.ts — generated by Better Auth CLI
// Run: npx better-auth generate  (auto-generates this)
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)

```typescript
// lib/auth.ts
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(),          // 2FA/TOTP support
    organization(),       // multi-tenant organizations
    admin(),              // admin panel + user management
  ],

  session: {
    expiresIn: 60 * 60 * 24 * 7,  // 7 days
    updateAge: 60 * 60 * 24,       // refresh session daily
    cookieCache: {
      enabled: true,
      maxAge: 60 * 5,              // cache session for 5 minutes
    },
  },

  trustedOrigins: [process.env.NEXT_PUBLIC_APP_URL!],
})

export type Session = typeof auth.$Infer.Session
export type User = typeof auth.$Infer.Session.user
```

### Route handler (Next.js App Router)

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

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

### Client setup

```typescript
// lib/auth-client.ts
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

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

// Sign in with email/password
await authClient.signIn.email({ email, password })

// Sign in with OAuth
await authClient.signIn.social({ provider: 'google' })

// Sign up
await authClient.signUp.email({ email, password, name })

// Sign out
await authClient.signOut()

// Get session (hook)
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

```typescript
// In Server Components and Server Actions
import { auth } from '@/lib/auth'
import { headers } from 'next/headers'

// Server Component
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>
}

// Server Action
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

```typescript
// middleware.ts
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)

```typescript
// Enable 2FA for a user
await authClient.twoFactor.enable({ password: currentPassword })

// Get TOTP URI (for QR code)
const { data } = await authClient.twoFactor.getTotpUri({ password })

// Verify TOTP code
await authClient.twoFactor.verifyTotp({ code: '123456' })

// Sign in with 2FA
const result = await authClient.signIn.email({ email, password })
if (result.data?.twoFactorRedirect) {
  // Prompt for TOTP code
  await authClient.twoFactor.verifyTotp({ code })
}
```

### Organizations (multi-tenant)

```typescript
// Create an organization
await authClient.organization.create({ name: 'Acme Corp', slug: 'acme' })

// Invite a member
await authClient.organization.inviteMember({
  email: 'colleague@acme.com',
  role: 'member',        // 'owner' | 'admin' | 'member'
  organizationId: org.id,
})

// Get active organization
const { data: activeOrg } = await authClient.organization.getActiveMember()

// Server-side: get organization from session
const session = await auth.api.getSession({ headers: await headers() })
const orgId = session?.session.activeOrganizationId
```

### CLI commands

```bash
# Generate database schema (creates auth-schema.ts)
npx better-auth generate

# Migrate database (applies auth tables)
npx better-auth migrate

# Open admin dashboard (dev only)
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

---

> **Work with us:** Claudient is backed by [Uitbreiden](https://uitbreiden.com/) — we build AI products and B2B solutions with developer communities.
> [uitbreiden.com](https://uitbreiden.com/) · [Reddit](https://www.reddit.com/r/uitbreiden/) · [YouTube](https://www.youtube.com/@UITBREIDEN)
