ovachiever

better-auth

19
1
# Install this skill:
npx skills add ovachiever/droid-tings --skill "better-auth"

Install specific skill from multi-skill repository

# Description

|

# SKILL.md


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-22
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: [email protected] (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: "[email protected]" },
  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:
    ```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,
});
}
3. **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;
```
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:
    ```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: { / ... / },
},
});
3. **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
  • 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:
- [email protected]
- [email protected]
- [email protected]
- [email protected]
- [email protected]
- @cloudflare/workers-types@latest
- [email protected]
- 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.

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.