🎯

better-auth

🎯Skill

from ovachiever/droid-tings

VibeIndex|
What it does

Builds robust, self-hosted authentication for Cloudflare Workers with social login, 2FA, passkeys, organizations, and role-based access control.

πŸ“¦

Part of

ovachiever/droid-tings(370 items)

better-auth

Installation

npxRun with npx
npx drizzle-kit generate
npxRun with npx
npx better-auth migrate
πŸ“– Extracted from docs: ovachiever/droid-tings
18Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

|

Overview

# 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

```typescript

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

});

}

```

Why CamelCasePlugin?

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.

```typescript

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

```typescript

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

},

},

})

```

πŸ“š Official Docs: https://www.better-auth.com/docs/integrations/tanstack

---

Available Plugins (v1.3+)

Better Auth provides plugins for advanced authentication features:

| Plugin | Import | Description | Docs |

|--------|--------|-------------|------|

| OIDC Provider | better-auth/plugins | Build your own OpenID Connect provider (become an OAuth provider for other apps) | [πŸ“š](https://www.better-auth.com/docs/plugins/oidc-provider) |

| SSO | better-auth/plugins | Enterprise Single Sign-On with OIDC, OAuth2, and SAML 2.0 support | [πŸ“š](https://www.better-auth.com/docs/plugins/sso) |

| Stripe | better-auth/plugins | Payment and subscription management (stable as of v1.3+) | [πŸ“š](https://www.better-auth.com/docs/plugins/stripe) |

| MCP | better-auth/plugins | Act as OAuth provider for Model Context Protocol (MCP) clients | [πŸ“š](https://www.better-auth.com/docs/plugins/mcp) |

| Expo | better-auth/expo | React Native/Expo integration with secure cookie management | [πŸ“š](https://www.better-auth.com/docs/integrations/expo) |

---

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/email | POST | Register with email/password |

| /sign-in/email | POST | Authenticate with email/password |

| /sign-out | POST | Logout user |

| /change-password | POST | Update password (requires current password) |

| /forget-password | POST | Initiate password reset flow |

| /reset-password | POST | Complete password reset with token |

| /send-verification-email | POST | Send email verification link |

| /verify-email | GET | Verify email with token (?token=) |

| /get-session | GET | Retrieve current session |

| /list-sessions | GET | Get all active user sessions |

| /revoke-session | POST | End specific session |

| /revoke-other-sessions | POST | End all sessions except current |

| /revoke-sessions | POST | End all user sessions |

| /update-user | POST | Modify user profile (name, image) |

| /change-email | POST | Update email address |

| /set-password | POST | Add password to OAuth-only account |

| /delete-user | POST | Remove user account |

| /list-accounts | GET | Get linked authentication providers |

| /link-social | POST | Connect OAuth provider to account |

| /unlink-account | POST | Disconnect provider |

#### Social OAuth Endpoints

| Endpoint | Method | Description |

|----------|--------|-------------|

| /sign-in/social | POST | Initiate OAuth flow (provider specified in body) |

| /callback/:provider | GET | OAuth callback handler (e.g., /callback/google) |

| /get-access-token | GET | Retrieve provider access token |

Example OAuth flow:

```typescript

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

```typescript

import { twoFactor } from "better-auth/plugins";

```

| Endpoint | Method | Description |

|----------|--------|-------------|

| /two-factor/enable | POST | Activate 2FA for user |

| /two-factor/disable | POST | Deactivate 2FA |

| /two-factor/get-totp-uri | GET | Get QR code URI for authenticator app |

| /two-factor/verify-totp | POST | Validate TOTP code from authenticator |

| /two-factor/send-otp | POST | Send OTP via email |

| /two-factor/verify-otp | POST | Validate email OTP |

| /two-factor/generate-backup-codes | POST | Create recovery codes |

| /two-factor/verify-backup-code | POST | Use backup code for login |

| /two-factor/view-backup-codes | GET | View current backup codes |

πŸ“š Docs: https://www.better-auth.com/docs/plugins/2fa

##### Organization Plugin (Multi-Tenant SaaS)

```typescript

import { organization } from "better-auth/plugins";

```

Organizations (10 endpoints):

| Endpoint | Method | Description |

|----------|--------|-------------|

| /organization/create | POST | Create organization |

| /organization/list | GET | List user's organizations |

| /organization/get-full | GET | Get complete org details |

| /organization/update | PUT | Modify organization |

| /organization/delete | DELETE | Remove organization |

| /organization/check-slug | GET | Verify slug availability |

| /organization/set-active | POST | Set active organization context |

Members (8 endpoints):

| Endpoint | Method | Description |

|----------|--------|-------------|

| /organization/list-members | GET | Get organization members |

| /organization/add-member | POST | Add member directly |

| /organization/remove-member | DELETE | Remove member |

| /organization/update-member-role | PUT | Change member role |

| /organization/get-active-member | GET | Get current member info |

| /organization/leave | POST | Leave organization |

Invitations (7 endpoints):

| Endpoint | Method | Description |

|----------|--------|-------------|

| /organization/invite-member | POST | Send invitation email |

| /organization/accept-invitation | POST | Accept invite |

| /organization/reject-invitation | POST | Reject invite |

| /organization/cancel-invitation | POST | Cancel pending invite |

| /organization/get-invitation | GET | Get invitation details |

| /organization/list-invitations | GET | List org invitations |

| /organization/list-user-invitations | GET | List user's pending invites |

Teams (8 endpoints):

| Endpoint | Method | Description |

|----------|--------|-------------|

| /organization/create-team | POST | Create team within org |

| /organization/list-teams | GET | List organization teams |

| /organization/update-team | PUT | Modify team |

| /organization/remove-team | DELETE | Remove team |

| /organization/set-active-team | POST | Set active team context |

| /organization/list-team-members | GET | List team members |

| /organization/add-team-member | POST | Add member to team |

| /organization/remove-team-member | DELETE | Remove team member |

Permissions & Roles (6 endpoints):

| Endpoint | Method | Description |

|----------|--------|-------------|

| /organization/has-permission | POST | Check if user has permission |

| /organization/create-role | POST | Create custom role |

| /organization/delete-role | DELETE | Delete custom role |

| /organization/list-roles | GET | List all roles |

| /organization/get-role | GET | Get role details |

| /organization/update-role | PUT | Modify role permissions |

πŸ“š Docs: https://www.better-auth.com/docs/plugins/organization

##### Admin Plugin

```typescript

import { admin } from "better-auth/plugins";

```

| Endpoint | Method | Description |

|----------|--------|-------------|

| /admin/create-user | POST | Create user as admin |

| /admin/list-users | GET | List all users (with filters/pagination) |

| /admin/set-role | POST | Assign user role |

| /admin/set-user-password | POST | Change user password |

| /admin/update-user | PUT | Modify user details |

| /admin/remove-user | DELETE | Delete user account |

| /admin/ban-user | POST | Ban user account |

| /admin/unban-user | POST | Unban user |

| /admin/list-user-sessions | GET | Get user's active sessions |

| /admin/revoke-user-session | DELETE | End specific user session |

| /admin/revoke-user-sessions | DELETE | End all user sessions |

| /admin/impersonate-user | POST | Start impersonating user |

| /admin/stop-impersonating | POST | End impersonation session |

πŸ“š Docs: https://www.better-auth.com/docs/plugins/admin

##### Other Plugin Endpoints

Passkey Plugin (5 endpoints) - [Docs](https://www.better-auth.com/docs/plugins/passkey):

  • /passkey/add, /sign-in/passkey, /passkey/list, /passkey/delete, /passkey/update

Magic Link Plugin (2 endpoints) - [Docs](https://www.better-auth.com/docs/plugins/magic-link):

  • /sign-in/magic-link, /magic-link/verify

Username Plugin (2 endpoints) - [Docs](https://www.better-auth.com/docs/plugins/username):

  • /sign-in/username, /username/is-available

Phone Number Plugin (5 endpoints) - [Docs](https://www.better-auth.com/docs/plugins/phone-number):

  • /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](https://www.better-auth.com/docs/plugins/email-otp):

  • /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](https://www.better-auth.com/docs/plugins/anonymous):

  • /sign-in/anonymous

JWT Plugin (2 endpoints) - [Docs](https://www.better-auth.com/docs/plugins/jwt):

  • /token (get JWT), /jwks (public key for verification)

OpenAPI Plugin (2 endpoints) - [Docs](https://www.better-auth.com/docs/plugins/open-api):

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

```typescript

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

2FA Plugin:

```typescript

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

});

```

Organization Plugin:

```typescript

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

});

```

Admin Plugin:

```typescript

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

```typescript

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:

```typescript

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

Programmatic access:

```typescript

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)

With better-auth:

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

```typescript

// ❌ 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:

```bash

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

With Kysely:

```typescript

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

```typescript

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

},

},

},

});

}

```

Add to wrangler.toml:

```toml

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

```typescript

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:

```typescript

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

For Drizzle approach:

```bash

npm install better-auth drizzle-orm drizzle-kit @cloudflare/workers-types

```

For Kysely approach:

```bash

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:

```typescript

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

Click the link below to verify your email:

Verify Email

`,

});

},

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:

```typescript

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:

```typescript

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:

```typescript

// src/db/index.ts

import { drizzle, type DrizzleD1Database } from "drizzle-orm/d1";

import * as schema from "./schema";

export type Database = DrizzleD1Database;

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:

```bash

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

```typescript

// 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'] })

}

```

When to use:

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

```typescript

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

Key differences:

  • Clerk: Third-party service β†’ better-auth: Self-hosted
  • Clerk: Proprietary β†’ better-auth: Open source
  • Clerk: Monthly cost β†’ better-auth: Free

Migration steps:

  1. Export user data from Clerk (CSV or API)
  2. Import into better-auth database:

```typescript

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

});

}

```

  1. Replace Clerk SDK with better-auth client:

```typescript

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

```

  1. Update middleware for session verification
  2. 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:

  1. Database schema: Auth.js and better-auth use similar schemas, but column names differ
  2. Replace configuration:

```typescript

// 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: { / ... / },

},

});

```

  1. Update client hooks:

```typescript

// Before

import { useSession } from "next-auth/react";

// After

import { authClient } from "@/lib/auth-client";

const { data: session } = authClient.useSession();

```

---

Additional Resources

Official Documentation

  • Homepage: https://better-auth.com
  • Introduction: https://www.better-auth.com/docs/introduction
  • Installation: https://www.better-auth.com/docs/installation
  • Basic Usage: https://www.better-auth.com/docs/basic-usage

Core Concepts

  • Session Management: https://www.better-auth.com/docs/concepts/session-management
  • Users & Accounts: https://www.better-auth.com/docs/concepts/users-accounts
  • Client SDK: https://www.better-auth.com/docs/concepts/client
  • Plugins System: https://www.better-auth.com/docs/concepts/plugins

Authentication Methods

  • Email & Password: https://www.better-auth.com/docs/authentication/email-password
  • OAuth Providers: https://www.better-auth.com/docs/concepts/oauth

Plugin Documentation

Core Plugins:

  • 2FA (Two-Factor): https://www.better-auth.com/docs/plugins/2fa
  • Organization: https://www.better-auth.com/docs/plugins/organization
  • Admin: https://www.better-auth.com/docs/plugins/admin
  • Multi-Session: https://www.better-auth.com/docs/plugins/multi-session
  • API Key: https://www.better-auth.com/docs/plugins/api-key
  • Generic OAuth: https://www.better-auth.com/docs/plugins/generic-oauth

Passwordless Plugins:

  • Passkey: https://www.better-auth.com/docs/plugins/passkey
  • Magic Link: https://www.better-auth.com/docs/plugins/magic-link
  • Email OTP: https://www.better-auth.com/docs/plugins/email-otp
  • Phone Number: https://www.better-auth.com/docs/plugins/phone-number
  • Anonymous: https://www.better-auth.com/docs/plugins/anonymous

Advanced Plugins:

  • Username: https://www.better-auth.com/docs/plugins/username
  • JWT: https://www.better-auth.com/docs/plugins/jwt
  • OpenAPI: https://www.better-auth.com/docs/plugins/open-api
  • OIDC Provider: https://www.better-auth.com/docs/plugins/oidc-provider
  • SSO: https://www.better-auth.com/docs/plugins/sso
  • Stripe: https://www.better-auth.com/docs/plugins/stripe
  • MCP: https://www.better-auth.com/docs/plugins/mcp

Framework Integrations

  • TanStack Start: https://www.better-auth.com/docs/integrations/tanstack
  • Expo (React Native): https://www.better-auth.com/docs/integrations/expo

Community & Support

  • GitHub: https://github.com/better-auth/better-auth (22.4k ⭐)
  • Examples: https://github.com/better-auth/better-auth/tree/main/examples
  • Discord: https://discord.gg/better-auth
  • Changelog: https://github.com/better-auth/better-auth/releases

Related Documentation

  • Drizzle ORM: https://orm.drizzle.team/docs/get-started-sqlite
  • Kysely: https://kysely.dev/

---

Production Examples

Verified working D1 repositories (all use Drizzle or Kysely):

  1. zpg6/better-auth-cloudflare - Drizzle + D1 (includes CLI)
  2. zwily/example-react-router-cloudflare-d1-drizzle-better-auth - Drizzle + D1
  3. foxlau/react-router-v7-better-auth - Drizzle + D1
  4. matthewlynch/better-auth-react-router-cloudflare-d1 - Kysely + D1

None use a direct d1Adapter - all require Drizzle/Kysely.

---

Version Compatibility

Tested with:

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

Breaking changes: Check changelog when upgrading: https://github.com/better-auth/better-auth/releases

---

Token Efficiency:

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