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 コミュニティが作成したAIスキルを発見・探索
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
直接コマンドでは確認用 Prompt が省略されます。実行前にソースを確認してください。
npx skills add https://github.com/ovachiever/droid-tings --skill better-authコマンドは1行のまま表示されます。コピー前に横へスクロールして全体を確認してください。
ローカルで確認しますか?SkillsMP が現在取得できるファイルをダウンロードできます。
Zipをダウンロード ダウンロード中... このリポジトリの他の Skills 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.
applying-brand-guidelines This skill applies consistent corporate branding and styling to all generated documents including colors, fonts, layouts, and messaging
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.