Better auth
ClaudeSkillz: For when you need skills, but lazier
npx -y skills add jackspace/ClaudeSkillz --skill better-authAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 15 stars15 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Production-ready authentication framework for TypeScript with first-class Cloudflare D1 support. Use this skill when building auth systems as a self-hosted alternative to Clerk or Auth.js, particularly for Cloudflare Workers projects. Supports social providers (Google, GitHub, Microsoft, Apple), email/password, magic links, 2FA, passkeys, organizations, and RBAC. Prevents 10+ common authentication errors including session serialization issues, CORS misconfigurations, D1 adapter setup, social provider OAuth flows, and JWT token handling. Keywords: better-auth, authentication, cloudflare d1 auth, self-hosted auth, typescript auth, clerk alternative, auth.js alternative, social login, oauth providers, session management, jwt tokens, 2fa, two-factor, passkeys, webauthn, multi-tenant auth, organizations, teams, rbac, role-based access, google auth, github auth, microsoft auth, apple auth, magic links, email password, better-auth setup, session serialization error, cors auth, d1 adapter
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
29.5 KB, as published. Nobody here has run it
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: [email protected] (latest verified 2025-10-31)
Installation
Core Package
npm install better-auth
# or
pnpm add better-auth
# or
yarn add better-auth
Database Adapters
For Cloudflare D1 (Workers):
npm install @cloudflare/workers-types
For PostgreSQL:
npm install pg drizzle-orm
For MySQL/SQLite: Built-in adapters, no extra packages needed.
Social Providers (Optional)
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
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:
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:
# 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
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
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
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
'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):
'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:
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:
// 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)
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:
// Enable 2FA for user
const { data, error } = await authClient.twoFactor.enable({
method: 'totp'
})
// Verify code
await authClient.twoFactor.verify({
code: '123456'
})
Organizations & Teams
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:
// Create organization
await authClient.organization.create({
name: 'Acme Corp',
slug: 'acme'
})
// Invite member
await authClient.organization.inviteMember({
organizationId: 'org_123',
email: '[email protected]',
role: 'member'
})
// Check permissions
const canDelete = await authClient.organization.hasPermission({
organizationId: 'org_123',
permission: 'delete'
})
Multi-Tenant SaaS
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
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:
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
# Generate migration
npx better-auth migrate --database d1
# This creates: migrations/0001_initial.sql
Apply migration:
# 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):
-- 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
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:
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
- Create OAuth credentials: https://console.cloud.google.com/apis/credentials
- Authorized redirect URI:
https://yourdomain.com/api/auth/callback/google - Environment variables:
GOOGLE_CLIENT_ID=your-client-id GOOGLE_CLIENT_SECRET=your-client-secret
Configuration:
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
scope: ['email', 'profile'] // Optional
}
}
GitHub OAuth
- Create OAuth app: https://github.com/settings/developers
- Authorization callback URL:
https://yourdomain.com/api/auth/callback/github - Environment variables:
GITHUB_CLIENT_ID=your-client-id GITHUB_CLIENT_SECRET=your-client-secret
Configuration:
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!
}
}
Microsoft OAuth
npm install @better-auth/microsoft
- Azure Portal: https://portal.azure.com → App registrations
- Redirect URI:
https://yourdomain.com/api/auth/callback/microsoft - Environment variables:
MICROSOFT_CLIENT_ID=your-client-id MICROSOFT_CLIENT_SECRET=your-client-secret MICROSOFT_TENANT_ID=common # or your tenant ID
Configuration:
import { microsoft } from '@better-auth/microsoft'
socialProviders: {
microsoft: microsoft({
clientId: process.env.MICROSOFT_CLIENT_ID!,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
tenantId: process.env.MICROSOFT_TENANT_ID!
})
}
Migration Guides
From Clerk
Key differences:
- Clerk: Third-party service → better-auth: Self-hosted
- Clerk: Proprietary → better-auth: Open source
- Clerk: Monthly cost → better-auth: Free
Migration steps:
- Export user data from Clerk (CSV or API)
- Import into better-auth database:
// migration script const clerkUsers = await fetchClerkUsers() for (const clerkUser of clerkUsers) { await db.insert(users).values({ id: clerkUser.id, email: clerkUser.email, emailVerified: clerkUser.email_verified, name: clerkUser.first_name + ' ' + clerkUser.last_name, image: clerkUser.profile_image_url }) } - Replace Clerk SDK with better-auth client:
// Before (Clerk) import { useUser } from '@clerk/nextjs' const { user } = useUser() // After (better-auth) import { useSession } from 'better-auth/client' const { data: session } = useSession() const user = session?.user - Update middleware for session verification
- Configure social providers (same OAuth apps, different config)
From Auth.js (NextAuth)
Key differences:
- Auth.js: Limited features → better-auth: Comprehensive (2FA, orgs, etc.)
- Auth.js: Callbacks-heavy → better-auth: Plugin-based
- Auth.js: Session handling varies → better-auth: Consistent
Migration steps:
- Database schema: Auth.js and better-auth use similar schemas, but column names differ
-- Map Auth.js to better-auth ALTER TABLE users RENAME COLUMN emailVerified TO email_verified; -- etc. - Replace configuration:
// Before (Auth.js) import NextAuth from 'next-auth' import GoogleProvider from 'next-auth/providers/google' export default NextAuth({ providers: [GoogleProvider({ /* ... */ })] }) // After (better-auth) import { betterAuth } from 'better-auth' export const auth = betterAuth({ socialProviders: { google: { /* ... */ } } }) - Update client hooks:
// Before import { useSession } from 'next-auth/react' // After import { useSession } from 'better-auth/client'
Known Issues & Solutions
Issue 1: D1 Eventual Consistency
Problem: Session reads immediately after write may return stale data in D1.
Symptoms: User logs in but getSession() returns null on next request.
Solution: Use Cloudflare KV for session storage (strong consistency):
import { betterAuth } from 'better-auth'
export const auth = betterAuth({
database: d1Adapter(env.DB), // Users, accounts
session: {
storage: {
get: async (sessionId) => {
const session = await env.SESSIONS_KV.get(sessionId)
return session ? JSON.parse(session) : null
},
set: async (sessionId, session, ttl) => {
await env.SESSIONS_KV.put(
sessionId,
JSON.stringify(session),
{ expirationTtl: ttl }
)
},
delete: async (sessionId) => {
await env.SESSIONS_KV.delete(sessionId)
}
}
}
})
Source: https://github.com/better-auth/better-auth/issues/147
Issue 2: CORS for SPA Applications
Problem: CORS errors when auth API is on different origin than frontend.
Symptoms: Access-Control-Allow-Origin errors in browser console.
Solution: Configure CORS headers in Worker:
import { Hono } from 'hono'
import { cors } from 'hono/cors'
const app = new Hono<{ Bindings: Env }>()
app.use('/api/auth/*', cors({
origin: ['https://yourdomain.com', 'http://localhost:3000'],
credentials: true, // Allow cookies
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
}))
app.all('/api/auth/*', async (c) => {
const auth = betterAuth({ /* ... */ })
return auth.handler(c.req.raw)
})
Source: https://better-auth.com/docs/guides/cors
Issue 3: Session Serialization in Workers
Problem: Can't serialize complex session objects in Cloudflare Workers.
Symptoms: DataCloneError or session data missing.
Solution: Keep session data minimal and JSON-serializable:
export const auth = betterAuth({
database: d1Adapter(env.DB),
session: {
// Only include serializable fields
fields: {
userId: true,
email: true,
role: true
// Don't include: functions, Dates, complex objects
}
}
})
Issue 4: OAuth Redirect URI Mismatch
Problem: Social sign-in fails with "redirect_uri_mismatch" error.
Symptoms: Google/GitHub OAuth returns error after user consent.
Solution: Ensure exact match in OAuth provider settings:
Provider setting: https://yourdomain.com/api/auth/callback/google
better-auth URL: https://yourdomain.com/api/auth/callback/google
❌ Wrong: http vs https, trailing slash, subdomain mismatch
✅ Right: Exact character-for-character match
Check better-auth callback URL:
// It's always: {baseURL}/api/auth/callback/{provider}
const callbackURL = `${process.env.NEXT_PUBLIC_API_URL}/api/auth/callback/google`
console.log('Configure this URL in Google Console:', callbackURL)
Issue 5: Email Verification Not Sending
Problem: Email verification links never arrive.
Symptoms: User signs up, but no email received.
Solution: Implement sendVerificationEmail handler:
export const auth = betterAuth({
database: /* ... */,
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendVerificationEmail: async ({ user, url, token }) => {
// Use your email service (SendGrid, Resend, etc.)
await sendEmail({
to: user.email,
subject: 'Verify your email',
html: `
<p>Click the link below to verify your email:</p>
<a href="${url}">Verify Email</a>
<p>Or use this code: ${token}</p>
`
})
}
}
})
For Cloudflare: Use Cloudflare Email Routing or external service (Resend, SendGrid).
Issue 6: JWT Token Expiration
Problem: Session expires too quickly or never expires.
Symptoms: User logged out unexpectedly or session persists after logout.
Solution: Configure session expiration:
export const auth = betterAuth({
database: /* ... */,
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days (in seconds)
updateAge: 60 * 60 * 24 // Update session every 24 hours
}
})
Issue 7: Password Hashing Performance
Problem: Sign-up/login slow on Cloudflare Workers.
Symptoms: Auth requests take >1 second.
Solution: better-auth uses bcrypt by default, which is CPU-intensive. For Workers, ensure proper async handling:
// better-auth handles this internally, but if custom:
import bcrypt from 'bcryptjs'
// Use async version (not sync)
const hash = await bcrypt.hash(password, 10) // ✅
const isValid = await bcrypt.compare(password, hash) // ✅
// Don't use:
const hash = bcrypt.hashSync(password, 10) // ❌ (blocks)
Alternative: Use better-auth's built-in hashing (already optimized).
Issue 8: Social Provider Scope Issues
Problem: Social sign-in succeeds but missing user data (name, avatar).
Symptoms: session.user.name is null after Google/GitHub sign-in.
Solution: Request additional scopes:
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
scope: ['openid', 'email', 'profile'] // Include 'profile' for name/image
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
scope: ['user:email', 'read:user'] // 'read:user' for full profile
}
}
Issue 9: Multi-Tenant Data Leakage
Problem: Users see data from other tenants.
Symptoms: User in Org A sees Org B's data.
Solution: Always filter queries by tenant ID:
import { multiTenant } from 'better-auth/plugins'
export const auth = betterAuth({
database: /* ... */,
plugins: [
multiTenant({
tenantIdHeader: 'x-tenant-id',
isolateData: true // Enforces tenant isolation
})
]
})
// In API routes
app.get('/api/data', async (c) => {
const session = await auth.getSession(c.req.raw)
const tenantId = c.req.header('x-tenant-id')
// ALWAYS filter by tenant
const data = await db.query.items.findMany({
where: eq(items.tenantId, tenantId)
})
return c.json(data)
})
Issue 10: Rate Limit False Positives
Problem: Legitimate users blocked by rate limiting.
Symptoms: "Too many requests" errors for normal usage.
Solution: Use IP + user ID for rate limit keys:
import { rateLimit } from 'better-auth/plugins'
plugins: [
rateLimit({
window: 60,
max: 10,
keyGenerator: (req) => {
// Combine IP and user ID (if authenticated)
const ip = req.headers.get('cf-connecting-ip') || 'unknown'
const userId = req.session?.userId || 'anonymous'
return `${ip}:${userId}`
}
})
]
Comparison: better-auth vs Alternatives
| Feature | better-auth | Clerk | Auth.js |
|---|---|---|---|
| Hosting | Self-hosted | Third-party | Self-hosted |
| Cost | Free (OSS) | $25/mo+ | Free (OSS) |
| Cloudflare D1 | ✅ First-class | ❌ No | ✅ Adapter |
| Social Auth | ✅ 10+ providers | ✅ Many | ✅ Many |
| 2FA/Passkeys | ✅ Plugin | ✅ Built-in | ⚠️ Limited |
| Organizations | ✅ Plugin | ✅ Built-in | ❌ No |
| Multi-tenant | ✅ Plugin | ✅ Yes | ❌ No |
| RBAC | ✅ Plugin | ✅ Yes | ⚠️ Custom |
| Magic Links | ✅ Built-in | ✅ Yes | ✅ Yes |
| Email/Password | ✅ Built-in | ✅ Yes | ✅ Yes |
| Session Management | ✅ JWT + DB | ✅ JWT | ✅ JWT + DB |
| TypeScript | ✅ First-class | ✅ Yes | ✅ Yes |
| Framework Support | ✅ Agnostic | ⚠️ React-focused | ✅ Agnostic |
| Vendor Lock-in | ✅ None | ❌ High | ✅ None |
| Customization | ✅ Full control | ⚠️ Limited | ✅ Full control |
| Production Ready | ✅ Yes | ✅ Yes | ✅ Yes |
Recommendation:
- Use better-auth if: Self-hosted, Cloudflare D1, want full control, avoid vendor lock-in
- Use Clerk if: Want managed service, don't mind cost, need fastest setup
- Use Auth.js if: Already using Next.js, basic needs, familiar with it
Best Practices
Security
- Always use HTTPS in production (no exceptions)
- Rotate secrets regularly:
# Generate new secret openssl rand -base64 32 # Update in Wrangler wrangler secret put BETTER_AUTH_SECRET - Validate email domains for sign-up:
emailAndPassword: { enabled: true, validate: async (email) => { const blockedDomains = ['tempmail.com', 'guerrillamail.com'] const domain = email.split('@')[1] if (blockedDomains.includes(domain)) { throw new Error('Email domain not allowed') } } } - Enable CSRF protection (enabled by default in better-auth)
- Use rate limiting for auth endpoints
- Log auth events for security monitoring:
onSuccess: async (user, action) => { await logAuthEvent({ userId: user.id, action, // 'sign-in', 'sign-up', 'password-change' timestamp: new Date(), ipAddress: req.headers.get('cf-connecting-ip') }) }
Performance
-
Cache session lookups (use KV for Workers):
const session = await env.SESSIONS_KV.get(sessionId) if (session) return JSON.parse(session) // Fallback to DB if not in cache const dbSession = await db.query.sessions.findFirst(/* ... */) await env.SESSIONS_KV.put(sessionId, JSON.stringify(dbSession)) -
Use indexes on frequently queried fields:
CREATE INDEX idx_sessions_user_id ON sessions(userId); CREATE INDEX idx_accounts_provider ON accounts(provider, providerAccountId); -
Minimize session data (only essential fields)
-
Use CDN for auth endpoints (cache public routes):
// Cache GET /api/auth/session for 5 minutes c.header('Cache-Control', 'public, max-age=300')
Development Workflow
-
Use environment-specific configs:
const isDev = process.env.NODE_ENV === 'development' export const auth = betterAuth({ database: /* ... */, baseURL: isDev ? 'http://localhost:3000' : 'https://yourdomain.com', session: { expiresIn: isDev ? 60 * 60 * 24 * 365 // 1 year for dev : 60 * 60 * 24 * 7 // 7 days for prod } }) -
Test social auth locally with ngrok:
ngrok http 3000 # Use ngrok URL as redirect URI in OAuth provider -
Seed test users for development:
// seed.ts const testUsers = [ { email: '[email protected]', password: 'password123', role: 'admin' }, { email: '[email protected]', password: 'password123', role: 'user' } ] for (const user of testUsers) { await authClient.signUp.email(user) }
Bundled Resources
This skill includes the following reference implementations:
scripts/setup-d1.sh- Automated D1 database setup for Cloudflare Workersreferences/cloudflare-worker-example.ts- Complete Worker with auth + protected routesreferences/nextjs-api-route.ts- Next.js API route patternreferences/react-client-hooks.tsx- React components with auth hooksreferences/drizzle-schema.ts- Drizzle ORM schema for better-auth tablesassets/auth-flow-diagram.md- Visual flow diagrams for OAuth, email verification
Use Read tool to access these files when needed.
Token Efficiency
Without this skill: ~15,000 tokens (setup trial-and-error, debugging CORS, D1 adapter, OAuth flows) With this skill: ~4,500 tokens (direct implementation from patterns) Savings: ~70% (10,500 tokens)
Errors prevented: 10 common issues documented with solutions
Additional Resources
- Official Docs: https://better-auth.com
- GitHub: https://github.com/better-auth/better-auth
- Examples: https://github.com/better-auth/better-auth/tree/main/examples
- Discord: https://discord.gg/better-auth
- Migration Guides: https://better-auth.com/docs/migrations
Version Compatibility
Tested with:
[email protected]@cloudflare/workers-types@latest[email protected][email protected]- Node.js 18+, Bun 1.0+
Breaking changes: Check changelog when upgrading: https://github.com/better-auth/better-auth/releases
Last verified: 2025-10-31 | Skill version: 1.0.0