Skip to main content 首页 创作者 ovachiever droid-tings better-auth
better-auth Build authentication systems for TypeScript/Cloudflare Workers with social auth, 2FA, passkeys, organizations, and RBAC. Self-hosted alternative to Clerk/Auth.js.
IMPORTANT: Requires Drizzle ORM or Kysely for D1 - no direct D1 adapter. v1.4.0 (Nov 2025) adds stateless sessions, ESM-only (breaking), JWT key rotation, SCIM provisioning. v1.3 adds SSO/SAML, multi-team support.
Use when: self-hosting auth on Cloudflare D1, migrating from Clerk, implementing multi-tenant SaaS, or troubleshooting D1 adapter errors, session serialization, OAuth flows, TanStack Start cookie issues, nanostore session invalidation.
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/ovachiever/droid-tings --skill better-auth命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... Build backend AI with Vercel AI SDK v5/v6. Covers v6 beta (Agent abstraction, tool approval, reranking),
v4→v5 migration (breaking changes), latest models (GPT-5/5.1, Claude 4.x, Gemini 2.5), Workers startup
fix, and 12 error solutions (AI_APICallError, AI_NoObjectGeneratedError, streamText silent errors).
Use when: implementing AI SDK v5/v6, migrating v4→v5, troubleshooting errors, fixing Workers startup
issues, or updating to latest models.
Build React chat interfaces with Vercel AI SDK v5/v6. Covers v6 beta (agent integration, tool approval,
auto-submit), v4→v5 migration (breaking changes), useChat/useCompletion/useObject/useAssistant hooks,
and 12 UI error solutions (stream parsing, stale body values, React update depth).
Use when: implementing AI SDK v5/v6 chat UIs, migrating v4→v5, troubleshooting "useChat failed to parse
stream", "useChat no response", or "stale body values" errors, or integrating OpenAI assistants.
name better-auth description Build authentication systems for TypeScript/Cloudflare Workers with social auth, 2FA, passkeys, organizations, and RBAC. Self-hosted alternative to Clerk/Auth.js.
IMPORTANT: Requires Drizzle ORM or Kysely for D1 - no direct D1 adapter. v1.4.0 (Nov 2025) adds stateless sessions, ESM-only (breaking), JWT key rotation, SCIM provisioning. v1.3 adds SSO/SAML, multi-team support.
Use when: self-hosting auth on Cloudflare D1, migrating from Clerk, implementing multi-tenant SaaS, or troubleshooting D1 adapter errors, session serialization, OAuth flows, TanStack Start cookie issues, nanostore session invalidation.
license MIT metadata {"version":"3.0.0","last_verified":"2025-11-22T00:00:00.000Z","production_tested":"multiple (zpg6/better-auth-cloudflare, zwily/example-react-router-cloudflare-d1-drizzle-better-auth, foxlau/react-router-v7-better-auth, matthewlynch/better-auth-react-router-cloudflare-d1)","package_version":"1.4.0","token_savings":"~80%","errors_prevented":13,"official_docs":"https://better-auth.com","github":"https://github.com/better-auth/better-auth","breaking_changes":"v1.4.0 - ESM-only (Nov 2025), v1.3 - Multi-team table changes (July 2025), v2.0.0 - D1 adapter patterns (Drizzle/Kysely required)","keywords":["better-auth","authentication","cloudflare-d1","drizzle-orm","kysely","self-hosted-auth","typescript-auth","clerk-alternative","authjs-alternative","social-auth","oauth","session-management","jwt","2fa","passkeys","multi-tenant","organizations","rbac"]} allowed-tools ["Read","Write","Edit","Bash","Glob","Grep"]
better-auth - D1 Adapter & Error Prevention Guide
Package : better-auth@1.4.0 (Nov 22, 2025)
Breaking Changes : ESM-only (v1.4.0), Multi-team table changes (v1.3), D1 requires Drizzle/Kysely (no direct adapter)
⚠️ CRITICAL: D1 Adapter Requirement
better-auth DOES NOT have d1Adapter(). You MUST use:
Drizzle ORM (recommended): drizzleAdapter(db, { provider: "sqlite" })
Kysely : new Kysely({ dialect: new D1Dialect({ database: env.DB }) })
See Issue #1 below for details.
What's New in v1.4.0 (Nov 22, 2025)
Major Features:
Stateless session management - Sessions without database storage
ESM-only package ⚠️ Breaking: CommonJS no longer supported
JWT key rotation - Automatic key rotation for enhanced security
SCIM provisioning - Enterprise user provisioning protocol
@standard-schema/spec - Replaces ZodType for validation
CaptchaFox integration - Built-in CAPTCHA support
Automatic server-side IP detection
Cookie-based account data storage
Multiple passkey origins support
RP-Initiated Logout endpoint (OIDC)
📚 Docs : https://www.better-auth.com/changelogs
What's New in v1.3 (July 2025)
Major Features:
SSO with SAML 2.0 - Enterprise single sign-on (moved to separate @better-auth/sso package)
Multi-team support ⚠️ Breaking: teamId removed from member table, new teamMembers table required
Additional fields - Custom fields for organization/member/invitation models
Performance improvements and bug fixes
📚 Docs : https://www.better-auth.com/blog/1-3
Alternative: Kysely Adapter Pattern
If you prefer Kysely over Drizzle:
File : src/auth.ts
import { betterAuth } from "better-auth";
import { Kysely, CamelCasePlugin } from "kysely";
import { D1Dialect } from "kysely-d1";
type Env = {
DB: D1Database;
BETTER_AUTH_SECRET: string;
// ... other env vars
};
export function createAuth(env: Env) {
return betterAuth({
secret: env.BETTER_AUTH_SECRET,
// Kysely with D1Dialect
database: {
db: new Kysely({
dialect: new D1Dialect({
database: env.DB,
}),
plugins: [
// CRITICAL: Required if using Drizzle schema with snake_case
new CamelCasePlugin(),
],
}),
type: "sqlite",
},
emailAndPassword: {
enabled: true,
},
// ... other config
});
}
If your Drizzle schema uses snake_case column names (e.g., email_verified), but better-auth expects camelCase (e.g., emailVerified), the CamelCasePlugin automatically converts between the two.
Framework Integrations
TanStack Start ⚠️ CRITICAL : TanStack Start requires the reactStartCookies plugin to handle cookie setting properly.
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { reactStartCookies } from "better-auth/react-start";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "sqlite" }),
plugins: [
twoFactor(),
organization(),
reactStartCookies(), // ⚠️ MUST be LAST plugin
],
});
Why it's needed : TanStack Start uses a special cookie handling system. Without this plugin, auth functions like signInEmail() and signUpEmail() won't set cookies properly, causing authentication to fail.
Important : The reactStartCookies plugin must be the last plugin in the array .
API Route Setup (/src/routes/api/auth/$.ts):
import { auth } from '@/lib/auth'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/api/auth/$')({
server: {
handlers: {
GET: ({ request }) => auth.handler(request),
POST: ({ request }) => auth.handler(request),
},
},
})
Available Plugins (v1.3+) Better Auth provides plugins for advanced authentication features:
Plugin Import Description Docs OIDC Provider better-auth/pluginsBuild your own OpenID Connect provider (become an OAuth provider for other apps) 📚 SSO better-auth/pluginsEnterprise Single Sign-On with OIDC, OAuth2, and SAML 2.0 support 📚 Stripe better-auth/pluginsPayment and subscription management (stable as of v1.3+) 📚 MCP better-auth/pluginsAct as OAuth provider for Model Context Protocol (MCP) clients 📚 Expo better-auth/expoReact Native/Expo integration with secure cookie management 📚
API Reference
Overview: What You Get For Free When you call auth.handler(), better-auth automatically exposes 80+ production-ready REST endpoints at /api/auth/*. Every endpoint is also available as a server-side method via auth.api.* for programmatic use.
This dual-layer API system means:
Clients (React, Vue, mobile apps) call HTTP endpoints directly
Server-side code (middleware, background jobs) uses auth.api.* methods
Zero boilerplate - no need to write auth endpoints manually
Time savings : Building this from scratch = ~220 hours. With better-auth = ~4-8 hours. 97% reduction.
Auto-Generated HTTP Endpoints All endpoints are automatically exposed at /api/auth/* when using auth.handler().
Core Authentication Endpoints Endpoint Method Description /sign-up/emailPOST Register with email/password /sign-in/emailPOST Authenticate with email/password /sign-outPOST Logout user /change-passwordPOST Update password (requires current password) /forget-passwordPOST Initiate password reset flow /reset-passwordPOST Complete password reset with token /send-verification-emailPOST Send email verification link /verify-emailGET Verify email with token (?token=<token>) /get-sessionGET Retrieve current session /list-sessionsGET Get all active user sessions /revoke-sessionPOST End specific session /revoke-other-sessionsPOST End all sessions except current /revoke-sessionsPOST End all user sessions /update-userPOST Modify user profile (name, image) /change-emailPOST Update email address /set-passwordPOST Add password to OAuth-only account /delete-userPOST Remove user account /list-accountsGET Get linked authentication providers /link-socialPOST Connect OAuth provider to account /unlink-accountPOST Disconnect provider
Social OAuth Endpoints Endpoint Method Description /sign-in/socialPOST Initiate OAuth flow (provider specified in body) /callback/:providerGET OAuth callback handler (e.g., /callback/google) /get-access-tokenGET Retrieve provider access token
// Client initiates
await authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
// better-auth handles redirect to Google
// Google redirects back to /api/auth/callback/google
// better-auth creates session automatically
Plugin Endpoints
Two-Factor Authentication (2FA Plugin) import { twoFactor } from "better-auth/plugins";
Endpoint Method Description /two-factor/enablePOST Activate 2FA for user /two-factor/disablePOST Deactivate 2FA /two-factor/get-totp-uriGET Get QR code URI for authenticator app /two-factor/verify-totpPOST Validate TOTP code from authenticator /two-factor/send-otpPOST Send OTP via email /two-factor/verify-otpPOST Validate email OTP /two-factor/generate-backup-codesPOST Create recovery codes /two-factor/verify-backup-codePOST Use backup code for login /two-factor/view-backup-codesGET View current backup codes
Organization Plugin (Multi-Tenant SaaS) import { organization } from "better-auth/plugins";
Organizations (10 endpoints):
Endpoint Method Description /organization/createPOST Create organization /organization/listGET List user's organizations /organization/get-fullGET Get complete org details /organization/updatePUT Modify organization /organization/deleteDELETE Remove organization /organization/check-slugGET Verify slug availability /organization/set-activePOST Set active organization context
Endpoint Method Description /organization/list-membersGET Get organization members /organization/add-memberPOST Add member directly /organization/remove-memberDELETE Remove member /organization/update-member-rolePUT Change member role /organization/get-active-memberGET Get current member info /organization/leavePOST Leave organization
Invitations (7 endpoints):
Endpoint Method Description /organization/invite-memberPOST Send invitation email /organization/accept-invitationPOST Accept invite /organization/reject-invitationPOST Reject invite /organization/cancel-invitationPOST Cancel pending invite /organization/get-invitationGET Get invitation details /organization/list-invitationsGET List org invitations /organization/list-user-invitationsGET List user's pending invites
Endpoint Method Description /organization/create-teamPOST Create team within org /organization/list-teamsGET List organization teams /organization/update-teamPUT Modify team /organization/remove-teamDELETE Remove team /organization/set-active-teamPOST Set active team context /organization/list-team-membersGET List team members /organization/add-team-memberPOST Add member to team /organization/remove-team-memberDELETE Remove team member
Permissions & Roles (6 endpoints):
Endpoint Method Description /organization/has-permissionPOST Check if user has permission /organization/create-rolePOST Create custom role /organization/delete-roleDELETE Delete custom role /organization/list-rolesGET List all roles /organization/get-roleGET Get role details /organization/update-rolePUT Modify role permissions
Admin Plugin import { admin } from "better-auth/plugins";
Endpoint Method Description /admin/create-userPOST Create user as admin /admin/list-usersGET List all users (with filters/pagination) /admin/set-rolePOST Assign user role /admin/set-user-passwordPOST Change user password /admin/update-userPUT Modify user details /admin/remove-userDELETE Delete user account /admin/ban-userPOST Ban user account /admin/unban-userPOST Unban user /admin/list-user-sessionsGET Get user's active sessions /admin/revoke-user-sessionDELETE End specific user session /admin/revoke-user-sessionsDELETE End all user sessions /admin/impersonate-userPOST Start impersonating user /admin/stop-impersonatingPOST End impersonation session
Other Plugin Endpoints Passkey Plugin (5 endpoints) - Docs :
/passkey/add, /sign-in/passkey, /passkey/list, /passkey/delete, /passkey/update
Magic Link Plugin (2 endpoints) - Docs :
/sign-in/magic-link, /magic-link/verify
Username Plugin (2 endpoints) - Docs :
/sign-in/username, /username/is-available
Phone Number Plugin (5 endpoints) - Docs :
/sign-in/phone-number, /phone-number/send-otp, /phone-number/verify, /phone-number/request-password-reset, /phone-number/reset-password
Email OTP Plugin (6 endpoints) - Docs :
/email-otp/send-verification-otp, /email-otp/check-verification-otp, /sign-in/email-otp, /email-otp/verify-email, /forget-password/email-otp, /email-otp/reset-password
Anonymous Plugin (1 endpoint) - Docs :
JWT Plugin (2 endpoints) - Docs :
/token (get JWT), /jwks (public key for verification)
OpenAPI Plugin (2 endpoints) - Docs :
/reference (interactive API docs with Scalar UI)
/generate-openapi-schema (get OpenAPI spec as JSON)
Server-Side API Methods (auth.api.*) Every HTTP endpoint has a corresponding server-side method. Use these for:
Server-side middleware (protecting routes)
Background jobs (user cleanup, notifications)
Admin operations (bulk user management)
Custom auth flows (programmatic session creation)
Core API Methods // Authentication
await auth.api.signUpEmail({
body: { email, password, name },
headers: request.headers,
});
await auth.api.signInEmail({
body: { email, password, rememberMe: true },
headers: request.headers,
});
await auth.api.signOut({ headers: request.headers });
// Session Management
const session = await auth.api.getSession({ headers: request.headers });
await auth.api.listSessions({ headers: request.headers });
await auth.api.revokeSession({
body: { token: "session_token_here" },
headers: request.headers,
});
// User Management
await auth.api.updateUser({
body: { name: "New Name", image: "https://..." },
headers: request.headers,
});
await auth.api.changeEmail({
body: { newEmail: "newemail@example.com" },
headers: request.headers,
});
await auth.api.deleteUser({
body: { password: "current_password" },
headers: request.headers,
});
// Account Linking
await auth.api.linkSocialAccount({
body: { provider: "google" },
headers: request.headers,
});
await auth.api.unlinkAccount({
body: { providerId: "google", accountId: "google_123" },
headers: request.headers,
});
Plugin API Methods // Enable 2FA
const { totpUri, backupCodes } = await auth.api.enableTwoFactor({
body: { issuer: "MyApp" },
headers: request.headers,
});
// Verify TOTP code
await auth.api.verifyTOTP({
body: { code: "123456", trustDevice: true },
headers: request.headers,
});
// Generate backup codes
const { backupCodes } = await auth.api.generateBackupCodes({
headers: request.headers,
});
// Create organization
const org = await auth.api.createOrganization({
body: { name: "Acme Corp", slug: "acme" },
headers: request.headers,
});
// Add member
await auth.api.addMember({
body: {
userId: "user_123",
role: "admin",
organizationId: org.id,
},
headers: request.headers,
});
// Check permissions
const hasPermission = await auth.api.hasPermission({
body: {
organizationId: org.id,
permission: "users:delete",
},
headers: request.headers,
});
// List users with pagination
const users = await auth.api.listUsers({
query: {
search: "john",
limit: 10,
offset: 0,
sortBy: "createdAt",
sortOrder: "desc",
},
headers: request.headers,
});
// Ban user
await auth.api.banUser({
body: {
userId: "user_123",
reason: "Violation of ToS",
expiresAt: new Date("2025-12-31"),
},
headers: request.headers,
});
// Impersonate user (for admin support)
const impersonationSession = await auth.api.impersonateUser({
body: {
userId: "user_123",
expiresIn: 3600, // 1 hour
},
headers: request.headers,
});
When to Use Which Use Case Use HTTP Endpoints Use auth.api.* Methods Client-side auth ✅ Yes ❌ No Server middleware ❌ No ✅ Yes Background jobs ❌ No ✅ Yes Admin dashboards ✅ Yes (from client) ✅ Yes (from server) Custom auth flows ❌ No ✅ Yes Mobile apps ✅ Yes ❌ No API routes ✅ Yes (proxy to handler) ✅ Yes (direct calls)
Example: Protected Route Middleware
import { Hono } from "hono";
import { createAuth } from "./auth";
import { createDatabase } from "./db";
const app = new Hono<{ Bindings: Env }>();
// Middleware using server-side API
app.use("/api/protected/*", async (c, next) => {
const db = createDatabase(c.env.DB);
const auth = createAuth(db, c.env);
// Use server-side method
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
return c.json({ error: "Unauthorized" }, 401);
}
// Attach to context
c.set("user", session.user);
c.set("session", session.session);
await next();
});
// Protected route
app.get("/api/protected/profile", async (c) => {
const user = c.get("user");
return c.json({ user });
});
Discovering Available Endpoints Use the OpenAPI plugin to see all endpoints in your configuration:
import { betterAuth } from "better-auth";
import { openAPI } from "better-auth/plugins";
export const auth = betterAuth({
database: /* ... */,
plugins: [
openAPI(), // Adds /api/auth/reference endpoint
],
});
Interactive documentation : Visit http://localhost:8787/api/auth/reference
This shows a Scalar UI with:
✅ All available endpoints grouped by feature
✅ Request/response schemas with types
✅ Try-it-out functionality (test endpoints in browser)
✅ Authentication requirements
✅ Code examples in multiple languages
const schema = await auth.api.generateOpenAPISchema();
console.log(JSON.stringify(schema, null, 2));
// Returns full OpenAPI 3.0 spec
Quantified Time Savings Building from scratch (manual implementation):
Core auth endpoints (sign-up, sign-in, OAuth, sessions): 40 hours
Email verification & password reset: 10 hours
2FA system (TOTP, backup codes, email OTP): 20 hours
Organizations (teams, invitations, RBAC): 60 hours
Admin panel (user management, impersonation): 30 hours
Testing & debugging: 50 hours
Security hardening: 20 hours
Total manual effort : ~220 hours (5.5 weeks full-time)
Initial setup: 2-4 hours
Customization & styling: 2-4 hours
Total with better-auth : 4-8 hours
Savings : ~97% development time
Key Takeaway better-auth provides 80+ production-ready endpoints covering:
✅ Core authentication (20 endpoints)
✅ 2FA & passwordless (15 endpoints)
✅ Organizations & teams (35 endpoints)
✅ Admin & user management (15 endpoints)
✅ Social OAuth (auto-configured callbacks)
✅ OpenAPI documentation (interactive UI)
You write zero endpoint code. Just configure features and call auth.handler().
Known Issues & Solutions
Issue 1: "d1Adapter is not exported" Error Problem : Code shows import { d1Adapter } from 'better-auth/adapters/d1' but this doesn't exist.
Symptoms : TypeScript error or runtime error about missing export.
Solution : Use Drizzle or Kysely instead:
// ❌ WRONG - This doesn't exist
import { d1Adapter } from 'better-auth/adapters/d1'
database: d1Adapter(env.DB)
// ✅ CORRECT - Use Drizzle
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { drizzle } from 'drizzle-orm/d1'
const db = drizzle(env.DB, { schema })
database: drizzleAdapter(db, { provider: "sqlite" })
// ✅ CORRECT - Use Kysely
import { Kysely } from 'kysely'
import { D1Dialect } from 'kysely-d1'
database: {
db: new Kysely({ dialect: new D1Dialect({ database: env.DB }) }),
type: "sqlite"
}
Source : Verified from 4 production repositories using better-auth + D1
Issue 2: Schema Generation Fails Problem : npx better-auth migrate doesn't create D1-compatible schema.
Symptoms : Migration SQL has wrong syntax or doesn't work with D1.
Solution : Use Drizzle Kit to generate migrations:
# Generate migration from Drizzle schema
npx drizzle-kit generate
# Apply to D1
wrangler d1 migrations apply my-app-db --remote
Why : Drizzle Kit generates SQLite-compatible SQL that works with D1.
Issue 3: "CamelCase" vs "snake_case" Column Mismatch Problem : Database has email_verified but better-auth expects emailVerified.
Symptoms : Session reads fail, user data missing fields.
Solution : Use CamelCasePlugin with Kysely or configure Drizzle properly:
import { CamelCasePlugin } from "kysely";
new Kysely({
dialect: new D1Dialect({ database: env.DB }),
plugins: [new CamelCasePlugin()], // Converts between naming conventions
})
With Drizzle : Define schema with camelCase from the start (as shown in examples).
Issue 4: D1 Eventual Consistency Problem : Session reads immediately after write return stale data.
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 function createAuth(db: Database, env: Env) {
return betterAuth({
database: drizzleAdapter(db, { provider: "sqlite" }),
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);
},
},
},
});
}
[[kv_namespaces]]
binding = "SESSIONS_KV"
id = "your-kv-namespace-id"
Issue 5: CORS Errors 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 { cors } from "hono/cors";
app.use(
"/api/auth/*",
cors({
origin: ["https://yourdomain.com", "http://localhost:3000"],
credentials: true, // Allow cookies
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
})
);
Issue 6: 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 = `${env.BETTER_AUTH_URL}/api/auth/callback/google`;
console.log("Configure this URL in Google Console:", callbackURL);
Issue 7: Missing Dependencies Problem : TypeScript errors or runtime errors about missing packages.
Symptoms : Cannot find module 'drizzle-orm' or similar.
Solution : Install all required packages:
npm install better-auth drizzle-orm drizzle-kit @cloudflare/workers-types
npm install better-auth kysely kysely-d1 @cloudflare/workers-types
Issue 8: 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,
},
emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
// 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>
`,
});
},
sendOnSignUp: true,
autoSignInAfterVerification: true,
expiresIn: 3600, // 1 hour
},
});
For Cloudflare : Use Cloudflare Email Routing or external service (Resend, SendGrid).
Issue 9: Session Expires Too Quickly Problem : Session expires unexpectedly 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 10: Social Provider Missing User Data 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: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
scope: ["openid", "email", "profile"], // Include 'profile' for name/image
},
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
scope: ["user:email", "read:user"], // 'read:user' for full profile
},
}
Issue 11: TypeScript Errors with Drizzle Schema Problem : TypeScript complains about schema types.
Symptoms : Type 'DrizzleD1Database' is not assignable to...
Solution : Export proper types from database:
// src/db/index.ts
import { drizzle, type DrizzleD1Database } from "drizzle-orm/d1";
import * as schema from "./schema";
export type Database = DrizzleD1Database<typeof schema>;
export function createDatabase(d1: D1Database): Database {
return drizzle(d1, { schema });
}
Issue 12: Wrangler Dev Mode Not Working Problem : wrangler dev fails with database errors.
Symptoms : "Database not found" or migration errors in local dev.
Solution : Apply migrations locally first:
# Apply migrations to local D1
wrangler d1 migrations apply my-app-db --local
# Then run dev server
wrangler dev
Issue 13: User Data Updates Not Reflecting in UI (with TanStack Query) Problem : After updating user data (e.g., avatar, name), changes don't appear in useSession() despite calling queryClient.invalidateQueries().
Symptoms : Avatar image or user profile data appears stale after successful update. TanStack Query cache shows updated data, but better-auth session still shows old values.
Root Cause : better-auth uses nanostores for session state management, not TanStack Query. Calling queryClient.invalidateQueries() only invalidates React Query cache, not the better-auth nanostore.
Solution : Manually notify the nanostore after updating user data:
// Update user data
const { data, error } = await authClient.updateUser({
image: newAvatarUrl,
name: newName
})
if (!error) {
// Manually invalidate better-auth session state
authClient.$store.notify('$sessionSignal')
// Optional: Also invalidate React Query if using it for other data
queryClient.invalidateQueries({ queryKey: ['user-profile'] })
}
Using better-auth + TanStack Query together
Updating user profile fields (name, image, email)
Any operation that modifies session user data client-side
Alternative : Call refetch() from useSession(), but $store.notify() is more direct:
const { data: session, refetch } = authClient.useSession()
// After update
await refetch()
Note : $store is an undocumented internal API. This pattern is production-validated but may change in future better-auth versions.
Source : Community-discovered pattern, production use verified
Migration Guides
From Clerk
Clerk: Third-party service → better-auth: Self-hosted
Clerk: Proprietary → better-auth: Open source
Clerk: Monthly cost → better-auth: Free
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(user).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 { authClient } from "@/lib/auth-client";
const { data: session } = authClient.useSession();
const user = session?.user;
Update middleware for session verification
Configure social providers (same OAuth apps, different config)
From Auth.js (NextAuth)
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
Database schema : Auth.js and better-auth use similar schemas, but column names differ
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 { authClient } from "@/lib/auth-client";
const { data: session } = authClient.useSession();
Additional Resources
Official Documentation
Core Concepts
Authentication Methods
Plugin Documentation
Framework Integrations
Community & Support
Related Documentation
Production Examples Verified working D1 repositories (all use Drizzle or Kysely):
zpg6/better-auth-cloudflare - Drizzle + D1 (includes CLI)
zwily/example-react-router-cloudflare-d1-drizzle-better-auth - Drizzle + D1
foxlau/react-router-v7-better-auth - Drizzle + D1
matthewlynch/better-auth-react-router-cloudflare-d1 - Kysely + D1
None use a direct d1Adapter - all require Drizzle/Kysely.
Version Compatibility
better-auth@1.3.34
drizzle-orm@0.44.7
drizzle-kit@0.31.6
kysely@0.28.8
kysely-d1@0.4.0
@cloudflare/workers-types@latest
hono@4.0.0
Node.js 18+, Bun 1.0+
Without skill : ~28,000 tokens (D1 adapter errors, TanStack Start cookies, nanostore invalidation, OAuth flows, API discovery)
With skill : ~5,600 tokens (focused on errors + breaking changes + API reference)
Savings : ~80% (~22,400 tokens)
Errors prevented : 13 documented issues with exact solutions
Key value : D1 adapter requirement, v1.4.0/v1.3 breaking changes, TanStack Start fix, nanostore pattern, 80+ endpoint reference
Last verified : 2025-11-22 | Skill version : 3.0.0 | Changes : Added v1.4.0 (ESM-only, stateless sessions, SCIM) and v1.3 (SSO/SAML, multi-team) knowledge gaps. Removed tutorial/setup (~700 lines). Focused on error prevention + breaking changes + API reference.