Files
gh-jezweb-claude-skills-ski…/SKILL.md
2025-11-30 08:23:56 +08:00

37 KiB

name, description, license, metadata, allowed-tools
name description license metadata allowed-tools
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. MIT
version last_verified production_tested package_version token_savings errors_prevented official_docs github breaking_changes keywords
3.0.0 2025-11-22 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) 1.4.0 ~80% 13 https://better-auth.com https://github.com/better-auth/better-auth v1.4.0 - ESM-only (Nov 2025), v1.3 - Multi-team table changes (July 2025), v2.0.0 - D1 adapter patterns (Drizzle/Kysely required)
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
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
  });
}

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.

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

📚 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) 📚
SSO better-auth/plugins Enterprise Single Sign-On with OIDC, OAuth2, and SAML 2.0 support 📚
Stripe better-auth/plugins Payment and subscription management (stable as of v1.3+) 📚
MCP better-auth/plugins Act as OAuth provider for Model Context Protocol (MCP) clients 📚
Expo better-auth/expo React 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/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=<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:

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

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

  • /sign-in/anonymous

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

2FA Plugin:

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

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

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

Programmatic access:

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:

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

With Kysely:

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

Add to wrangler.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:

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:

For Drizzle approach:

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

For Kysely approach:

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

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:

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:
    // 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,
      });
    }
    
  3. 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;
    
  4. Update middleware for session verification
  5. 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:
    // 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: { /* ... */ },
      },
    });
    
  3. 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

Core Plugins:

Passwordless Plugins:

Advanced Plugins:

Framework Integrations

Community & Support


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.