|
| 1 | +# Configuration Reference |
| 2 | + |
| 3 | +## `withCloudflare(cloudflareOptions, authOptions)` |
| 4 | + |
| 5 | +Wraps your Better Auth config with Cloudflare integrations. The result is spread into `betterAuth()`: |
| 6 | + |
| 7 | +```typescript |
| 8 | +import { betterAuth } from "better-auth"; |
| 9 | +import { withCloudflare } from "better-auth-cloudflare"; |
| 10 | + |
| 11 | +const auth = betterAuth({ |
| 12 | + ...withCloudflare( |
| 13 | + { |
| 14 | + /* WithCloudflareOptions */ |
| 15 | + }, |
| 16 | + { |
| 17 | + /* BetterAuthOptions */ |
| 18 | + } |
| 19 | + ), |
| 20 | +}); |
| 21 | +``` |
| 22 | + |
| 23 | +> **Do not** add `cloudflare()` to your `plugins` array when using `withCloudflare` — it is injected automatically. Adding it manually results in a duplicate plugin. |
| 24 | +
|
| 25 | +### Override Behavior |
| 26 | + |
| 27 | +`withCloudflare` returns a merged config object. The following keys are **always set** by the wrapper and take precedence over values in `authOptions`: |
| 28 | + |
| 29 | +| Key | Behavior | |
| 30 | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | |
| 31 | +| `database` | Set from your `d1` / `d1Native` / `postgres` / `mysql` option. Omit `database` from `authOptions`. | |
| 32 | +| `secondaryStorage` | Set to `createKVStorage(kv)` when `kv` is provided, otherwise `undefined`. Omit from `authOptions`. | |
| 33 | +| `plugins` | The `cloudflare()` plugin is prepended to your `authOptions.plugins` array. | |
| 34 | +| `advanced` | Merges your `authOptions.advanced` with IP detection headers when `autoDetectIpAddress` is enabled. | |
| 35 | +| `session` | Merges your `authOptions.session`, forcing `storeSessionInDatabase: true` when `geolocationTracking` is enabled — even if you explicitly set it to `false`. | |
| 36 | + |
| 37 | +If you need a custom `secondaryStorage` that is not KV, omit the `kv` option and set `secondaryStorage` outside the spread: |
| 38 | + |
| 39 | +```typescript |
| 40 | +const auth = betterAuth({ |
| 41 | + ...withCloudflare(cloudflareOpts, authOpts), |
| 42 | + secondaryStorage: myCustomStorage, |
| 43 | +}); |
| 44 | +``` |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## `WithCloudflareOptions` |
| 49 | + |
| 50 | +Extends [`CloudflarePluginOptions`](#cloudflarepluginoptions) with database and KV configuration. |
| 51 | + |
| 52 | +### Database Options |
| 53 | + |
| 54 | +Only **one** database option may be provided — passing more than one throws at startup. All are optional; omitting them all is valid for CLI schema generation (`database` will be `undefined`). |
| 55 | + |
| 56 | +| Option | Type | Description | |
| 57 | +| ---------- | --------------------------------------- | -------------------------------------------------------------------- | |
| 58 | +| `d1` | `DrizzleConfig<typeof d1Drizzle>` | D1 via Drizzle ORM | |
| 59 | +| `d1Native` | `D1Database` | Native D1 binding (no Drizzle, uses better-auth's Kysely D1 dialect) | |
| 60 | +| `postgres` | `DrizzleConfig<typeof postgresDrizzle>` | Postgres via Hyperdrive + Drizzle | |
| 61 | +| `mysql` | `DrizzleConfig<typeof mysqlDrizzle>` | MySQL via Hyperdrive + Drizzle | |
| 62 | + |
| 63 | +### KV Option |
| 64 | + |
| 65 | +| Option | Type | Description | |
| 66 | +| ------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------- | |
| 67 | +| `kv` | `KVNamespace` | KV namespace for [secondary storage](#kv-secondary-storage). Automatically wired as `secondaryStorage` via `createKVStorage`. | |
| 68 | + |
| 69 | +### `DrizzleConfig<T>` |
| 70 | + |
| 71 | +```typescript |
| 72 | +type DrizzleConfig<T> = { |
| 73 | + db: ReturnType<T>; |
| 74 | + options?: Omit<DrizzleAdapterConfig, "provider">; |
| 75 | +}; |
| 76 | +``` |
| 77 | + |
| 78 | +The `provider` is inferred from which option you use (`"sqlite"` / `"pg"` / `"mysql"`). Common adapter options: `usePlural`, `debugLogs`. |
| 79 | + |
| 80 | +--- |
| 81 | + |
| 82 | +## `CloudflarePluginOptions` |
| 83 | + |
| 84 | +Inherited by `WithCloudflareOptions`. |
| 85 | + |
| 86 | +| Option | Type | Default | Description | |
| 87 | +| --------------------- | --------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------- | |
| 88 | +| `autoDetectIpAddress` | `boolean` | `true` | Adds `cf-connecting-ip` and `x-real-ip` to IP detection headers. | |
| 89 | +| `geolocationTracking` | `boolean` | `true` | Enriches sessions with geolocation fields. Overrides `session.storeSessionInDatabase` to `true`. | |
| 90 | +| `cf` | `CloudflareGeolocation \| Promise<…> \| null` | `undefined` | **Required** unless both options above are disabled. Typically `request.cf` (Hono) or `getCloudflareContext().cf` (OpenNext). | |
| 91 | +| `r2` | `R2Config` | `undefined` | R2 bucket configuration. See the [R2 File Storage Guide](./r2.md). | |
| 92 | + |
| 93 | +### `CloudflareGeolocation` |
| 94 | + |
| 95 | +When `geolocationTracking` is enabled, these optional `string` fields are added to the `session` table and populated on session creation from `cf`: |
| 96 | + |
| 97 | +```typescript |
| 98 | +interface CloudflareGeolocation { |
| 99 | + timezone?: string | null; |
| 100 | + city?: string | null; |
| 101 | + country?: string | null; |
| 102 | + region?: string | null; |
| 103 | + regionCode?: string | null; |
| 104 | + colo?: string | null; |
| 105 | + latitude?: string | null; |
| 106 | + longitude?: string | null; |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +This is the subset of Cloudflare's `IncomingRequestCfProperties` that the library extracts. You can pass the full `request.cf` object — only these fields are read. |
| 111 | + |
| 112 | +--- |
| 113 | + |
| 114 | +## KV Secondary Storage |
| 115 | + |
| 116 | +Passing `kv` to `withCloudflare` enables [Better Auth Secondary Storage](https://www.better-auth.com/docs/concepts/database#secondary-storage) backed by Cloudflare KV — used for rate limiting, session caching, and verification tokens. |
| 117 | + |
| 118 | +```typescript |
| 119 | +withCloudflare( |
| 120 | + { |
| 121 | + d1: { db, options: { usePlural: true } }, |
| 122 | + kv: env.KV, |
| 123 | + cf: request.cf, |
| 124 | + }, |
| 125 | + { |
| 126 | + rateLimit: { enabled: true, window: 60, max: 100 }, |
| 127 | + } |
| 128 | +); |
| 129 | +``` |
| 130 | + |
| 131 | +### `createKVStorage(kv)` |
| 132 | + |
| 133 | +If you need to wire secondary storage manually (without `withCloudflare`): |
| 134 | + |
| 135 | +```typescript |
| 136 | +import { createKVStorage, cloudflare } from "better-auth-cloudflare"; |
| 137 | + |
| 138 | +const auth = betterAuth({ |
| 139 | + database: myDatabase, |
| 140 | + secondaryStorage: createKVStorage(env.KV), |
| 141 | + plugins: [cloudflare({ cf: request.cf })], |
| 142 | +}); |
| 143 | +``` |
| 144 | + |
| 145 | +> **Note:** The standalone `cloudflare()` plugin does **not** throw when `cf` is missing — the geolocation endpoint returns a 404 instead. `withCloudflare` is stricter and throws at startup if `cf` is omitted while `autoDetectIpAddress` or `geolocationTracking` is enabled. |
| 146 | +
|
| 147 | +### KV TTL Limitation |
| 148 | + |
| 149 | +Cloudflare KV enforces a **minimum TTL of 60 seconds**. `createKVStorage` clamps lower values automatically and logs a warning. Configure rate limit `window` accordingly: |
| 150 | + |
| 151 | +```typescript |
| 152 | +rateLimit: { |
| 153 | + enabled: true, |
| 154 | + window: 60, // Must be >= 60 when using KV |
| 155 | + max: 100, |
| 156 | +}, |
| 157 | +``` |
| 158 | + |
| 159 | +Better Auth's built-in sign-in endpoints have their own default rate limit windows that may be lower than 60s, which causes KV write errors. Override them explicitly ([better-auth#5452](https://github.com/better-auth/better-auth/issues/5452)): |
| 160 | + |
| 161 | +```typescript |
| 162 | +rateLimit: { |
| 163 | + enabled: true, |
| 164 | + window: 60, |
| 165 | + max: 100, |
| 166 | + customRules: { |
| 167 | + "/sign-in/email": { window: 60, max: 5 }, |
| 168 | + "/sign-in/social": { window: 60, max: 5 }, |
| 169 | + }, |
| 170 | +}, |
| 171 | +``` |
| 172 | + |
| 173 | +--- |
| 174 | + |
| 175 | +## Database Examples |
| 176 | + |
| 177 | +### D1 with Drizzle |
| 178 | + |
| 179 | +```typescript |
| 180 | +import { drizzle } from "drizzle-orm/d1"; |
| 181 | + |
| 182 | +const db = drizzle(env.DATABASE, { schema }); |
| 183 | + |
| 184 | +withCloudflare( |
| 185 | + { d1: { db, options: { usePlural: true } }, cf: request.cf }, |
| 186 | + { |
| 187 | + /* auth options */ |
| 188 | + } |
| 189 | +); |
| 190 | +``` |
| 191 | + |
| 192 | +### Native D1 (No Drizzle) |
| 193 | + |
| 194 | +```typescript |
| 195 | +withCloudflare( |
| 196 | + { d1Native: env.DATABASE, cf: request.cf }, |
| 197 | + { |
| 198 | + /* auth options */ |
| 199 | + } |
| 200 | +); |
| 201 | +``` |
| 202 | + |
| 203 | +| | `d1Native` | `d1` (Drizzle) | |
| 204 | +| ----------------- | ---------------------------- | ------------------------- | |
| 205 | +| Bundle size | Smaller | Larger (includes Drizzle) | |
| 206 | +| Schema management | Manual SQL / better-auth CLI | Drizzle Kit migrations | |
| 207 | +| Type-safe queries | No | Yes | |
| 208 | + |
| 209 | +### Hyperdrive (Postgres) |
| 210 | + |
| 211 | +```typescript |
| 212 | +import { drizzle } from "drizzle-orm/postgres-js"; |
| 213 | +import postgres from "postgres"; |
| 214 | + |
| 215 | +const db = drizzle(postgres(env.HYPERDRIVE.connectionString), { schema }); |
| 216 | + |
| 217 | +withCloudflare( |
| 218 | + { postgres: { db }, cf: request.cf }, |
| 219 | + { |
| 220 | + /* auth options */ |
| 221 | + } |
| 222 | +); |
| 223 | +``` |
| 224 | + |
| 225 | +### Hyperdrive (MySQL) |
| 226 | + |
| 227 | +```typescript |
| 228 | +import { drizzle } from "drizzle-orm/mysql2"; |
| 229 | +import mysql from "mysql2/promise"; |
| 230 | + |
| 231 | +const db = drizzle(mysql.createPool(env.HYPERDRIVE.connectionString), { schema }); |
| 232 | + |
| 233 | +withCloudflare( |
| 234 | + { mysql: { db }, cf: request.cf }, |
| 235 | + { |
| 236 | + /* auth options */ |
| 237 | + } |
| 238 | +); |
| 239 | +``` |
| 240 | + |
| 241 | +--- |
| 242 | + |
| 243 | +## `wrangler.toml` Reference |
| 244 | + |
| 245 | +Complete example with all supported binding types. Include only what you need. |
| 246 | + |
| 247 | +```toml |
| 248 | +name = "my-auth-app" |
| 249 | +main = "src/index.ts" |
| 250 | +compatibility_date = "2025-03-01" |
| 251 | +compatibility_flags = ["nodejs_compat"] |
| 252 | + |
| 253 | +[observability] |
| 254 | +enabled = true |
| 255 | + |
| 256 | +[placement] |
| 257 | +mode = "smart" |
| 258 | + |
| 259 | +# D1 — Create with: wrangler d1 create my-auth-db |
| 260 | +[[d1_databases]] |
| 261 | +binding = "DATABASE" |
| 262 | +database_name = "my-auth-db" |
| 263 | +database_id = "<your-database-id>" |
| 264 | +migrations_dir = "drizzle" |
| 265 | + |
| 266 | +# KV — Create with: wrangler kv namespace create KV |
| 267 | +[[kv_namespaces]] |
| 268 | +binding = "KV" |
| 269 | +id = "<your-kv-namespace-id>" |
| 270 | + |
| 271 | +# R2 (optional) — Create with: wrangler r2 bucket create my-files |
| 272 | +[[r2_buckets]] |
| 273 | +binding = "R2_BUCKET" |
| 274 | +bucket_name = "my-files" |
| 275 | + |
| 276 | +# Hyperdrive (optional) — Create with: wrangler hyperdrive create my-hd --connection-string="..." |
| 277 | +# [[hyperdrive]] |
| 278 | +# binding = "HYPERDRIVE" |
| 279 | +# id = "<your-hyperdrive-id>" |
| 280 | + |
| 281 | +[vars] |
| 282 | +BETTER_AUTH_URL = "https://your-app.example.com" |
| 283 | +BETTER_AUTH_TRUSTED_ORIGINS = "https://your-app.example.com" |
| 284 | +``` |
| 285 | + |
| 286 | +### Binding Names and `env.d.ts` |
| 287 | + |
| 288 | +The `binding` value in `wrangler.toml` determines the property name on `env`. Declare them for type safety: |
| 289 | + |
| 290 | +```typescript |
| 291 | +import type { D1Database, Hyperdrive, KVNamespace, R2Bucket } from "@cloudflare/workers-types"; |
| 292 | + |
| 293 | +interface CloudflareBindings { |
| 294 | + DATABASE: D1Database; |
| 295 | + KV: KVNamespace; |
| 296 | + R2_BUCKET: R2Bucket; |
| 297 | + HYPERDRIVE: Hyperdrive; // Only if using Hyperdrive |
| 298 | + BETTER_AUTH_URL: string; |
| 299 | + BETTER_AUTH_TRUSTED_ORIGINS: string; |
| 300 | +} |
| 301 | +``` |
| 302 | + |
| 303 | +These names are configurable — if you change `binding = "KV"` to `binding = "AUTH_KV"` in `wrangler.toml`, update `env.d.ts` and your auth config to match. The [CLI](../cli/README.md) supports `--kv-binding`, `--d1-binding`, and `--r2-binding` flags for this. |
| 304 | + |
| 305 | +--- |
| 306 | + |
| 307 | +## Commonly Used Exports |
| 308 | + |
| 309 | +The main entry point (`better-auth-cloudflare`) re-exports all types and functions from the library. Commonly used: |
| 310 | + |
| 311 | +| Export | Kind | Description | |
| 312 | +| --------------------------- | -------- | -------------------------------------------------------------------------------- | |
| 313 | +| `withCloudflare` | function | Wraps `BetterAuthOptions` with Cloudflare integrations (database, KV, plugin). | |
| 314 | +| `cloudflare` | function | Standalone Better Auth plugin for geolocation, IP detection, and R2. | |
| 315 | +| `createKVStorage` | function | Creates a `SecondaryStorage` backed by Cloudflare KV. | |
| 316 | +| `createR2Config` | function | Helper for creating a fully type-inferred `R2Config`. | |
| 317 | +| `CloudflareGeolocation` | type | The 8 geolocation fields extracted from `request.cf`. | |
| 318 | +| `CloudflareSession` | type | `Session` extended with geolocation fields. | |
| 319 | +| `CloudflareSessionResponse` | type | `{ session: CloudflareSession; user: User }` — shape of `/api/auth/get-session`. | |
| 320 | +| `CloudflarePluginOptions` | type | Options for the standalone `cloudflare()` plugin. | |
| 321 | +| `WithCloudflareOptions` | type | Options for the `withCloudflare` wrapper. | |
| 322 | +| `R2Config` | type | R2 bucket configuration. See the [R2 File Storage Guide](./r2.md). | |
| 323 | +| `FileMetadata` | type | Core file record shape stored in the database. | |
0 commit comments