better-auth Skill
Overview
better-auth is a comprehensive, framework-agnostic authentication and authorization library for TypeScript. It provides a complete auth solution with first-class support for Cloudflare D1, making it an excellent self-hosted alternative to Clerk or Auth.js.
Use this skill when:
- Building authentication for Cloudflare Workers + D1 applications
- Need a self-hosted, vendor-independent auth solution
- Migrating from Clerk (avoid vendor lock-in)
- Upgrading from Auth.js (need more features)
- Implementing multi-tenant SaaS with organizations/teams
- Require advanced features: 2FA, passkeys, RBAC, social auth
Package: better-auth@1.3.34 (latest verified 2025-10-31)
Installation
Core Package
bash
npm install better-auth
# or
pnpm add better-auth
# or
yarn add better-auth
Database Adapters
For Cloudflare D1 (Workers):
bash
npm install @cloudflare/workers-types
For PostgreSQL:
bash
npm install pg drizzle-orm
For MySQL/SQLite: Built-in adapters, no extra packages needed.
Social Providers (Optional)
bash
npm install @better-auth/google
npm install @better-auth/github
npm install @better-auth/microsoft
Quick Start Patterns
Pattern 1: Cloudflare Workers + D1
Use when: Building API on Cloudflare Workers with D1 database
File: src/worker.ts
typescript
import { betterAuth } from 'better-auth'
import { d1Adapter } from 'better-auth/adapters/d1'
import { Hono } from 'hono'
type Env = {
DB: D1Database
BETTER_AUTH_SECRET: string
GOOGLE_CLIENT_ID: string
GOOGLE_CLIENT_SECRET: string
}
const app = new Hono<{ Bindings: Env }>()
// Auth routes handler
app.all('/api/auth/*', async (c) => {
const auth = betterAuth({
database: d1Adapter(c.env.DB),
secret: c.env.BETTER_AUTH_SECRET,
// Basic auth methods
emailAndPassword: {
enabled: true,
requireEmailVerification: true
},
// Social providers
socialProviders: {
google: {
clientId: c.env.GOOGLE_CLIENT_ID,
clientSecret: c.env.GOOGLE_CLIENT_SECRET
}
}
})
return auth.handler(c.req.raw)
})
export default app
wrangler.toml:
toml
name = "my-app"
main = "src/worker.ts"
compatibility_date = "2024-01-01"
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "your-database-id"
[vars]
# Public vars here
# Secrets (use: wrangler secret put BETTER_AUTH_SECRET)
# - BETTER_AUTH_SECRET
# - GOOGLE_CLIENT_ID
# - GOOGLE_CLIENT_SECRET
Setup D1 Database:
bash
# Create database
wrangler d1 create my-app-db
# Generate migration SQL from better-auth
npx better-auth migrate --database d1
# Apply migration
wrangler d1 execute my-app-db --remote --file migrations/0001_initial.sql
Pattern 2: Next.js API Route
Use when: Building traditional Next.js app with PostgreSQL or D1
File: src/lib/auth.ts
typescript
import { betterAuth } from 'better-auth'
import { Pool } from 'pg'
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL
}),
secret: process.env.BETTER_AUTH_SECRET!,
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendVerificationEmail: async ({ user, url }) => {
// Send email with verification link
await sendEmail({
to: user.email,
subject: 'Verify your email',
html: `Click <a href="${url}">here</a> to verify`
})
}
},
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!
}
},
// Advanced features via plugins
plugins: [
twoFactor(),
organization(),
rateLimit()
]
})
File: src/app/api/auth/[...all]/route.ts
typescript
import { auth } from '@/lib/auth'
export const GET = auth.handler
export const POST = auth.handler
Pattern 3: React Client Integration
Use when: Need client-side auth state and actions
File: src/lib/auth-client.ts
typescript
import { createAuthClient } from 'better-auth/client'
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'
})
File: src/components/LoginForm.tsx
typescript
'use client'
import { authClient } from '@/lib/auth-client'
import { useState } from 'react'
export function LoginForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const { data, error } = await authClient.signIn.email({
email,
password
})
if (error) {
console.error('Login failed:', error)
return
}
// Redirect or update UI
window.location.href = '/dashboard'
}
const handleGoogleSignIn = async () => {
await authClient.signIn.social({
provider: 'google',
callbackURL: '/dashboard'
})
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Sign In</button>
<button type="button" onClick={handleGoogleSignIn}>
Sign in with Google
</button>
</form>
)
}
Use React Hook (if you have a session endpoint):
typescript
'use client'
import { useSession } from 'better-auth/client'
export function UserProfile() {
const { data: session, isPending } = useSession()
if (isPending) return <div>Loading...</div>
if (!session) return <div>Not authenticated</div>
return (
<div>
<p>Welcome, {session.user.email}</p>
<button onClick={() => authClient.signOut()}>
Sign Out
</button>
</div>
)
}
Pattern 4: Protected API Route (Middleware)
Use when: Need to verify session in API routes
Cloudflare Workers:
typescript
import { betterAuth } from 'better-auth'
import { d1Adapter } from 'better-auth/adapters/d1'
app.get('/api/protected', async (c) => {
const auth = betterAuth({
database: d1Adapter(c.env.DB),
secret: c.env.BETTER_AUTH_SECRET
})
const session = await auth.getSession(c.req.raw)
if (!session) {
return c.json({ error: 'Unauthorized' }, 401)
}
return c.json({
message: 'Protected data',
user: session.user
})
})
Next.js Middleware:
typescript
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
export async function middleware(request: NextRequest) {
const session = await auth.getSession(request)
if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*']
}
Advanced Features
Two-Factor Authentication (2FA)
typescript
import { betterAuth } from 'better-auth'
import { twoFactor } from 'better-auth/plugins'
export const auth = betterAuth({
database: /* ... */,
plugins: [
twoFactor({
methods: ['totp', 'sms'], // Time-based or SMS
issuer: 'MyApp'
})
]
})
Client:
typescript
// Enable 2FA for user
const { data, error } = await authClient.twoFactor.enable({
method: 'totp'
})
// Verify code
await authClient.twoFactor.verify({
code: '123456'
})
Organizations & Teams
typescript
import { betterAuth } from 'better-auth'
import { organization } from 'better-auth/plugins'
export const auth = betterAuth({
database: /* ... */,
plugins: [
organization({
roles: ['owner', 'admin', 'member'],
permissions: {
admin: ['read', 'write', 'delete'],
member: ['read']
}
})
]
})
Client:
typescript
// Create organization
await authClient.organization.create({
name: 'Acme Corp',
slug: 'acme'
})
// Invite member
await authClient.organization.inviteMember({
organizationId: 'org_123',
email: 'user@example.com',
role: 'member'
})
// Check permissions
const canDelete = await authClient.organization.hasPermission({
organizationId: 'org_123',
permission: 'delete'
})
Multi-Tenant SaaS
typescript
import { betterAuth } from 'better-auth'
import { multiTenant } from 'better-auth/plugins'
export const auth = betterAuth({
database: /* ... */,
plugins: [
multiTenant({
tenantIdHeader: 'x-tenant-id',
isolateData: true // Ensure tenant data isolation
})
]
})
Rate Limiting
typescript
import { betterAuth } from 'better-auth'
import { rateLimit } from 'better-auth/plugins'
export const auth = betterAuth({
database: /* ... */,
plugins: [
rateLimit({
window: 60, // 60 seconds
max: 5, // 5 requests per window
storage: 'database' // or 'memory'
})
]
})
For Cloudflare: Use KV for distributed rate limiting:
typescript
import { rateLimit } from 'better-auth/plugins'
plugins: [
rateLimit({
window: 60,
max: 5,
storage: {
get: async (key) => {
return await c.env.RATE_LIMIT_KV.get(key)
},
set: async (key, value, ttl) => {
await c.env.RATE_LIMIT_KV.put(key, value, { expirationTtl: ttl })
}
}
})
]
Database Setup
D1 Schema Migration
bash
# Generate migration
npx better-auth migrate --database d1
# This creates: migrations/0001_initial.sql
Apply migration:
bash
# Local
wrangler d1 execute my-app-db --local --file migrations/0001_initial.sql
# Production
wrangler d1 execute my-app-db --remote --file migrations/0001_initial.sql
Manual schema (if needed):
sql
-- better-auth core tables
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
emailVerified INTEGER DEFAULT 0,
name TEXT,
image TEXT,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL
);
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
userId TEXT NOT NULL,
expiresAt INTEGER NOT NULL,
ipAddress TEXT,
userAgent TEXT,
FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE accounts (
id TEXT PRIMARY KEY,
userId TEXT NOT NULL,
provider TEXT NOT NULL,
providerAccountId TEXT NOT NULL,
accessToken TEXT,
refreshToken TEXT,
expiresAt INTEGER,
FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE verification_tokens (
identifier TEXT NOT NULL,
token TEXT NOT NULL,
expires INTEGER NOT NULL,
PRIMARY KEY (identifier, token)
);
-- Additional tables for plugins (organizations, 2FA, etc.)
PostgreSQL with Drizzle
File: src/db/schema.ts
typescript
import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: text('id').primaryKey(),
email: text('email').unique().notNull(),
emailVerified: boolean('email_verified').default(false),
name: text('name'),
image: text('image'),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow()
})
// ... other tables
Setup:
typescript
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { betterAuth } from 'better-auth'
const client = postgres(process.env.DATABASE_URL!)
const db = drizzle(client)
export const auth = betterAuth({
database: db,
// ...
})
Social Provider Setup
Google OAuth