Add self-hosted authentication to a Cloudflare Workers + D1 project using better-auth.
Adds self-hosted authentication to Cloudflare Workers + D1 using better-auth with Drizzle ORM.
/plugin marketplace add jezweb/claude-skills/plugin install jezweb-better-auth-skills-better-auth@jezweb/claude-skillsAdd self-hosted authentication to a Cloudflare Workers + D1 project using better-auth.
Follow these steps to configure better-auth with Drizzle ORM and D1.
Verify the project has:
/drizzle-orm-d1/init first if not)npm install better-auth
npm install -D @better-auth/cli
Create src/lib/auth/index.ts:
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import type { DrizzleD1Database } from 'drizzle-orm/d1';
import * as schema from '@/db/schema';
export function createAuth(db: DrizzleD1Database<typeof schema>, env: Env) {
return betterAuth({
database: drizzleAdapter(db, {
provider: 'sqlite',
schema: {
user: schema.users,
session: schema.sessions,
account: schema.accounts,
verification: schema.verifications,
},
}),
secret: env.BETTER_AUTH_SECRET,
baseURL: env.BETTER_AUTH_URL,
emailAndPassword: {
enabled: true,
},
socialProviders: {
// Add providers as needed
// google: {
// clientId: env.GOOGLE_CLIENT_ID,
// clientSecret: env.GOOGLE_CLIENT_SECRET,
// },
},
});
}
Create src/lib/auth/cli.ts for schema generation:
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
const sqlite = new Database(':memory:');
const db = drizzle(sqlite);
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: 'sqlite' }),
emailAndPassword: { enabled: true },
});
Run schema generation:
npx @better-auth/cli generate --config ./src/lib/auth/cli.ts
Update src/db/schema.ts with generated tables:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
// Your existing tables...
// Auth tables (from better-auth generate)
export const users = sqliteTable('users', {
id: text('id').primaryKey(),
email: text('email').notNull().unique(),
emailVerified: integer('email_verified', { mode: 'boolean' }).default(false),
name: text('name'),
image: text('image'),
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
});
export const sessions = sqliteTable('sessions', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
token: text('token').notNull().unique(),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
});
export const accounts = sqliteTable('accounts', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
providerId: text('provider_id').notNull(),
providerAccountId: text('provider_account_id').notNull(),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
expiresAt: integer('expires_at', { mode: 'timestamp' }),
});
export const verifications = sqliteTable('verifications', {
id: text('id').primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
});
Add to your Hono app (src/index.ts):
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { createAuth } from './lib/auth';
import { createDb } from './db';
const app = new Hono<{ Bindings: Env }>();
// CORS for credentials
app.use('/api/auth/*', cors({
origin: (origin) => origin,
credentials: true,
}));
// Auth routes
app.all('/api/auth/*', async (c) => {
const db = createDb(c.env.DB);
const auth = createAuth(db, c.env);
return auth.handler(c.req.raw);
});
Add to wrangler.jsonc:
{
"vars": {
"BETTER_AUTH_URL": "http://localhost:8787"
}
}
Create .dev.vars:
BETTER_AUTH_SECRET=your-secret-at-least-32-characters-here
Set production secret:
echo "your-production-secret" | npx wrangler secret put BETTER_AUTH_SECRET
npm run db:generate
npm run db:migrate:local
npm run db:migrate:remote
Create src/client/lib/auth.ts:
import { createAuthClient } from 'better-auth/react';
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_API_URL || window.location.origin,
});
export const { useSession, signIn, signUp, signOut } = authClient;
ā
better-auth configured with D1!
š Added:
- src/lib/auth/index.ts (Auth configuration)
- Auth tables in schema (users, sessions, accounts, verifications)
- /api/auth/* routes (Auth endpoints)
š Client Usage:
import { useSession, signIn, signUp, signOut } from '@/lib/auth';
const { data: session } = useSession();
await signIn.email({ email, password });
await signUp.email({ email, password, name });
await signOut();
ā” Plugins Available:
- 2FA (twoFactor)
- Passkeys (passkey)
- Organizations (organization)
- RBAC (admin)
š Skill loaded: better-auth
- D1 via Drizzle adapter
- Session caching patterns
- Rate limiting included