This example demonstrates how to integrate Better Auth with Hono on Cloudflare Workers using the better-auth-cloudflare plugin.
- 🚀 Hono Framework: Lightning-fast web framework for Cloudflare Workers
- 🗄️ D1 Database Integration: SQLite database via Cloudflare D1
- 🔌 KV Storage Integration: Session caching via Cloudflare KV
- 📍 Automatic Geolocation Tracking: Enriches sessions with location data
- 🌐 Cloudflare IP Detection: Automatic IP address detection
- 👤 Anonymous Authentication: Built-in anonymous user authentication
- 🔐 Session Management: Secure session handling with geolocation
- Node.js 18+ and pnpm
- Cloudflare account with Workers and D1 enabled
- Wrangler CLI installed globally:
npm install -g wrangler
- Navigate to this directory:
cd examples/hono- Install dependencies:
pnpm install- Configure your Cloudflare bindings in
wrangler.toml:
[[d1_databases]]
binding = "DATABASE"
database_name = "your-database-name"
database_id = "your-database-id"
[[kv_namespaces]]
binding = "KV"
id = "your-kv-namespace-id"- Create a D1 database:
wrangler d1 create your-database-name-
Update the
database_idinwrangler.tomlwith the ID from the previous command. -
Create a KV namespace:
wrangler kv namespace create "KV"-
Update the KV
idinwrangler.tomlwith the ID from the previous command. -
Apply database migrations:
pnpm run db:migrate:prodDeploy to Cloudflare Workers:
pnpm run deploysrc/
├── auth/
│ └── index.ts # Better Auth configuration
├── db/
│ ├── index.ts # Database exports
│ ├── schema.ts # Combined schema
│ └── auth.schema.ts # Generated auth schema
├── env.d.ts # TypeScript environment types
└── index.ts # Hono application
drizzle/ # Database migrations
wrangler.toml # Cloudflare Worker configuration
pnpm run auth:generate- Generate auth schema from Better Auth configpnpm run auth:format- Format the generated auth schemapnpm run auth:update- Generate and format auth schema
pnpm run db:generate- Generate new database migrationspnpm run db:migrate:dev- Apply migrations to local D1 databasepnpm run db:migrate:prod- Apply migrations to production D1 databasepnpm run db:studio:dev- Open Drizzle Studio for local databasepnpm run db:studio:prod- Open Drizzle Studio for production database
pnpm run dev- Start development serverpnpm run deploy- Deploy to Cloudflare Workerspnpm run cf-typegen- Generate Cloudflare binding types
GET /- Demo page with anonymous authentication UIGET /health- Health check endpointGET /protected- Protected route demoALL /api/auth/*- All Better Auth routes (handled by better-auth)POST /api/auth/sign-in/anonymous- Anonymous loginPOST /api/auth/sign-out- Sign outGET /api/auth/get-session- Get current sessionGET /api/auth/cloudflare/geolocation- Get geolocation data
When geolocationTracking is enabled, user sessions automatically include:
timezone- User's timezonecity- User's citycountry- User's countryregion- User's region/stateregionCode- Region codecolo- Cloudflare colo data centerlatitude&longitude- Coordinates
The application uses Cloudflare bindings defined in wrangler.toml:
interface CloudflareBindings {
DATABASE: D1Database;
KV: KVNamespace;
}The auth configuration in src/auth/index.ts uses a simplified single-function approach that handles both CLI schema generation and runtime scenarios:
import type { D1Database, IncomingRequestCfProperties } from "@cloudflare/workers-types";
import { betterAuth } from "better-auth";
import { withCloudflare } from "better-auth-cloudflare";
import { anonymous } from "better-auth/plugins";
import { drizzleAdapter } from "@better-auth/drizzle-adapter";
import { drizzle } from "drizzle-orm/d1";
import { schema } from "../db";
import type { CloudflareBindings } from "../env";
// Single auth configuration that handles both CLI and runtime scenarios
function createAuth(env?: CloudflareBindings, cf?: IncomingRequestCfProperties, baseURL?: string) {
// Use actual DB for runtime, empty object for CLI
const db = env ? drizzle(env.DATABASE, { schema, logger: true }) : ({} as any);
return betterAuth({
baseURL,
...withCloudflare(
{
autoDetectIpAddress: true, // Auto-detect IP from Cloudflare headers
geolocationTracking: true, // Track geolocation in sessions
cf: cf || {},
d1: env
? {
db,
options: {
usePlural: true,
debugLogs: true,
},
}
: undefined,
kv: env?.KV,
},
{
emailAndPassword: {
enabled: true,
},
plugins: [anonymous()], // Enable anonymous authentication
rateLimit: {
enabled: true,
window: 60, // Minimum KV TTL is 60s
max: 100, // reqs/window
customRules: {
// https://github.com/better-auth/better-auth/issues/5452
"/sign-in/email": {
window: 60,
max: 100,
},
"/sign-in/social": {
window: 60,
max: 100,
},
},
},
}
),
// Only add database adapter for CLI schema generation
...(env
? {}
: {
database: drizzleAdapter({} as D1Database, {
provider: "sqlite",
usePlural: true,
debugLogs: true,
}),
}),
});
}
// Export for CLI schema generation
export const auth = createAuth();
// Export for runtime usage
export { createAuth };The baseURL is derived from each incoming request in the Hono middleware (new URL(c.req.url).origin) and passed to createAuth. On Cloudflare Workers, request.url reflects the actual URL the client connected to — Cloudflare's edge routes requests to your worker based on DNS and route configuration, not the HTTP Host header alone. Alternatively, you can set the BETTER_AUTH_URL environment variable and omit the baseURL parameter.