better-auth authentication for TypeScript with Cloudflare D1 via Drizzle ORM/Kysely. Use for self-hosted auth, social providers, 2FA, passkeys, RBAC, or encountering D1 adapter, schema generation, session, CORS, OAuth flow errors.
/plugin marketplace add secondsky/claude-skills/plugin install better-auth@claude-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/auth-flow-diagram.mdreferences/advanced-features.mdreferences/cloudflare-worker-drizzle.tsreferences/cloudflare-worker-kysely.tsreferences/configuration-guide.mdreferences/database-schema.tsreferences/error-catalog.mdreferences/framework-comparison.mdreferences/migration-guide-1.4.0.mdreferences/nextjs/README.mdreferences/nextjs/postgres-example.tsreferences/react-client-hooks.tsxreferences/setup-guide.mdscripts/setup-d1-drizzle.shStatus: Production Ready
Last Updated: 2025-11-27
Package: better-auth@1.4.3 (ESM-only)
Dependencies: Drizzle ORM or Kysely (required for D1)
Option 1: Drizzle ORM (Recommended)
bun add better-auth drizzle-orm drizzle-kit
Option 2: Kysely
bun add better-auth kysely @noxharmonium/kysely-d1
better-auth v1.4.0+ is ESM-only. Ensure:
package.json:
{
"type": "module"
}
Upgrading from v1.3.x? Load references/migration-guide-1.4.0.md
better-auth DOES NOT have a direct d1Adapter(). You MUST use either:
drizzleAdapter()// ❌ WRONG - This doesn't exist
import { d1Adapter } from 'better-auth/adapters/d1'
// ✅ CORRECT - Use Drizzle
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { drizzle } from 'drizzle-orm/d1'
1. Create D1 Database:
wrangler d1 create my-app-db
2. Define Schema (src/db/schema.ts):
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const user = sqliteTable("user", {
id: text().primaryKey(),
name: text().notNull(),
email: text().notNull().unique(),
emailVerified: integer({ mode: "boolean" }).notNull().default(false),
image: text(),
});
export const session = sqliteTable("session", {
id: text().primaryKey(),
userId: text().notNull().references(() => user.id, { onDelete: "cascade" }),
token: text().notNull(),
expiresAt: integer({ mode: "timestamp" }).notNull(),
});
// See references/database-schema.ts for complete schema
3. Initialize Auth (src/auth.ts):
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { drizzle } from "drizzle-orm/d1";
import * as schema from "./db/schema";
export function createAuth(env: { DB: D1Database; BETTER_AUTH_SECRET: string }) {
const db = drizzle(env.DB, { schema });
return betterAuth({
baseURL: env.BETTER_AUTH_URL,
secret: env.BETTER_AUTH_SECRET,
database: drizzleAdapter(db, { provider: "sqlite" }),
emailAndPassword: { enabled: true },
});
}
4. Create Worker (src/index.ts):
import { Hono } from "hono";
import { createAuth } from "./auth";
const app = new Hono<{ Bindings: Env }>();
app.all("/api/auth/*", async (c) => {
const auth = createAuth(c.env);
return auth.handler(c.req.raw);
});
export default app;
5. Deploy:
bunx drizzle-kit generate
wrangler d1 migrations apply my-app-db --remote
wrangler deploy
✅ Use Drizzle/Kysely adapter (d1Adapter doesn't exist)
✅ Use Drizzle Kit for migrations (not better-auth migrate)
✅ Set BETTER_AUTH_SECRET via wrangler secret put
✅ Configure CORS with credentials: true
✅ Match OAuth callback URLs exactly (no trailing slash)
✅ Apply migrations to local D1 before wrangler dev
✅ Use camelCase column names in schema
❌ Use d1Adapter or better-auth migrate with D1
❌ Forget CORS credentials or mismatch OAuth URLs
❌ Use snake_case columns without CamelCasePlugin
❌ Skip local migrations or hardcode secrets
❌ Leave sendVerificationEmail unimplemented
ESM-only (no CommonJS):
// package.json required
{ "type": "module" }
API Renames:
forgetPassword → requestPasswordReset/account-info → GET /account-infoCallback Signatures:
// v1.3.x: request parameter
sendVerificationEmail: async ({ user, url, request }) => {}
// v1.4.0+: ctx parameter
sendVerificationEmail: async ({ user, url, ctx }) => {}
Load references/migration-guide-1.4.0.md when upgrading from <1.4.0
Problem: Trying to use non-existent d1Adapter
Solution: Use drizzleAdapter or Kysely instead (see Quick Start above)
Problem: better-auth migrate doesn't work with D1
Solution: Use bunx drizzle-kit generate then wrangler d1 migrations apply
Problem: Database uses email_verified but better-auth expects emailVerified
Solution: Use camelCase in schema or add CamelCasePlugin to Kysely
Problem: Access-Control-Allow-Origin errors, cookies not sent
Solution: Configure CORS with credentials: true and correct origins
Problem: Social sign-in fails with "redirect_uri_mismatch"
Solution: Ensure exact match: https://yourdomain.com/api/auth/callback/google
Load references/error-catalog.md for all 12 errors with detailed solutions.
When: Basic authentication without social providers Quick Pattern:
// Client
await authClient.signIn.email({
email: "user@example.com",
password: "password123",
});
// Server - enable in config
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
}
Load: references/setup-guide.md → Step 5
When: Allow users to sign in with social accounts Quick Pattern:
// Client
await authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
// Server config
socialProviders: {
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
scope: ["openid", "email", "profile"],
},
}
Load: references/setup-guide.md → Step 5
When: Need to verify user is authenticated Quick Pattern:
app.get("/api/protected", async (c) => {
const auth = createAuth(c.env);
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
return c.json({ error: "Unauthorized" }, 401);
}
return c.json({ data: "protected", user: session.user });
});
Load: references/cloudflare-worker-drizzle.ts
When: Building SaaS with teams/organizations
Load: references/advanced-features.md → Organizations & Teams
When: Need extra security with 2FA/TOTP
Load: references/advanced-features.md → Two-Factor Authentication
Load references/setup-guide.md when:
Load references/error-catalog.md when:
Load references/advanced-features.md when:
Load references/cloudflare-worker-drizzle.ts when:
Load references/cloudflare-worker-kysely.ts when:
Load references/database-schema.ts when:
Load references/react-client-hooks.tsx when:
Load references/configuration-guide.md when:
Load references/framework-comparison.md when:
Load references/migration-guide-1.4.0.md when:
forgetPassword errors or ESM issuesQuick Config (ESM-only in v1.4.0+):
export const auth = betterAuth({
baseURL: env.BETTER_AUTH_URL,
secret: env.BETTER_AUTH_SECRET,
database: drizzleAdapter(db, { provider: "sqlite" }),
});
Load references/configuration-guide.md for:
Create auth client (src/lib/auth-client.ts):
import { createAuthClient } from "better-auth/client";
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_API_URL || "http://localhost:8787",
});
Use in React:
import { authClient } from "@/lib/auth-client";
export function UserProfile() {
const { data: session, isPending } = authClient.useSession();
if (isPending) return <div>Loading...</div>;
if (!session) return <div>Not authenticated</div>;
return (
<div>
<p>Welcome, {session.user.email}</p>
<button onClick={() => authClient.signOut()}>Sign Out</button>
</div>
);
}
Required:
better-auth@^1.4.3 - Core authentication framework (ESM-only)Choose ONE adapter:
drizzle-orm@^0.44.7 + drizzle-kit@^0.31.7 (recommended)kysely@^0.28.8 + @noxharmonium/kysely-d1@^0.4.0 (alternative)Optional:
@cloudflare/workers-types - TypeScript types for Workershono@^4.0.0 - Web framework for routing@better-auth/passkey - Passkey plugin (v1.4.0+, separate package)@better-auth/api-key - API key auth (v1.4.0+)Load references/framework-comparison.md for:
Verified working repositories (all use Drizzle or Kysely):
Note: Check each repo's better-auth version. Repos on v1.3.x need v1.4.0+ migration (see references/migration-guide-1.4.0.md). None use a direct d1Adapter - all require Drizzle/Kysely.
"type": "module" in package.json) - v1.4.0+ requiredQuestions? Issues?
references/error-catalog.md for all 12 errors and solutionsreferences/setup-guide.md for complete 8-step setupreferences/advanced-features.md for 2FA, organizations, and moreCreating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.