From 4bc12099143a113716dec09dc3ff7c0f01142059 Mon Sep 17 00:00:00 2001 From: Zach Grimaldi Date: Thu, 23 Oct 2025 07:44:37 -0400 Subject: [PATCH 1/2] fix: kv ttl min 60sec --- README.md | 38 +++++++++++++++++++++++++++ examples/hono/README.md | 14 +++++++++- examples/hono/src/auth/index.ts | 13 +++++++++ examples/opennextjs/README.md | 13 +++++++++ examples/opennextjs/src/auth/index.ts | 14 +++++++++- src/index.ts | 11 +++++++- 6 files changed, 100 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0b3858c..b289a11 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,19 @@ function createAuth(env?: CloudflareBindings, cf?: IncomingRequestCfProperties) }, 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, + }, + }, }, } ), @@ -341,6 +354,31 @@ If you provide a KV namespace in the `withCloudflare` configuration (as shown in Ensure your KV namespace (e.g., `USER_SESSIONS`) is correctly bound in your `wrangler.toml` file. +#### Important: KV TTL Limitation + +Cloudflare KV has a minimum TTL (Time To Live) requirement of **60 seconds**. If you're using KV for secondary storage with rate limiting enabled, you **must** configure your rate limit windows to be at least 60 seconds to prevent crashes: + +```typescript +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, + }, + }, +}, +``` + +The library automatically enforces this minimum and will log a warning if a TTL less than 60 seconds is attempted, but it's better to configure your rate limits correctly from the start. + ### 6. Set Up API Routes Create API routes to handle authentication requests. Better Auth provides a handler that can be used for various HTTP methods. diff --git a/examples/hono/README.md b/examples/hono/README.md index 02e1e1d..51d6f47 100644 --- a/examples/hono/README.md +++ b/examples/hono/README.md @@ -195,8 +195,20 @@ function createAuth(env?: CloudflareBindings, cf?: IncomingRequestCfProperties) { plugins: [anonymous()], // Enable anonymous authentication rateLimit: { - // Enable rate limiting 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, + }, + }, }, } ), diff --git a/examples/hono/src/auth/index.ts b/examples/hono/src/auth/index.ts index 5ae3d32..8fc061d 100644 --- a/examples/hono/src/auth/index.ts +++ b/examples/hono/src/auth/index.ts @@ -36,6 +36,19 @@ function createAuth(env?: CloudflareBindings, cf?: IncomingRequestCfProperties) plugins: [anonymous()], 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, + }, + }, }, } ), diff --git a/examples/opennextjs/README.md b/examples/opennextjs/README.md index 6ddef6a..23670a5 100644 --- a/examples/opennextjs/README.md +++ b/examples/opennextjs/README.md @@ -105,6 +105,19 @@ async function authBuilder() { }, 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, + }, + }, }, plugins: [openAPI()], } diff --git a/examples/opennextjs/src/auth/index.ts b/examples/opennextjs/src/auth/index.ts index 458d96b..90be810 100644 --- a/examples/opennextjs/src/auth/index.ts +++ b/examples/opennextjs/src/auth/index.ts @@ -71,7 +71,19 @@ async function authBuilder() { { rateLimit: { enabled: true, - // ... other rate limiting options + 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, + }, + }, }, plugins: [openAPI(), anonymous()], // ... other Better Auth options diff --git a/src/index.ts b/src/index.ts index cf2cd5f..89ed74f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -122,7 +122,16 @@ export const createKVStorage = (kv: KVNamespace): SecondaryStorage => { return kv.get(key); }, set: async (key: string, value: string, ttl?: number) => { - return kv.put(key, value, ttl ? { expirationTtl: ttl } : undefined); + if (ttl !== undefined) { + // Cloudflare KV requires TTL >= 60 seconds + const minTtl = 60; + if (ttl < minTtl) { + console.warn(`[BetterAuthCloudflare] TTL ${ttl}s is less than KV minimum of ${minTtl}s. Using ${minTtl}s instead.`); + ttl = minTtl; + } + return kv.put(key, value, { expirationTtl: ttl }); + } + return kv.put(key, value); }, delete: async (key: string) => { return kv.delete(key); From c60f21fe9a9754100207257842e72d9b67a29b54 Mon Sep 17 00:00:00 2001 From: Zach Grimaldi Date: Thu, 23 Oct 2025 07:53:16 -0400 Subject: [PATCH 2/2] fix: moved imports --- src/index.ts | 3 ++- src/schema.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 89ed74f..4b052f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import type { KVNamespace } from "@cloudflare/workers-types"; -import { type BetterAuthOptions, type BetterAuthPlugin, type SecondaryStorage, type Session } from "better-auth"; +import { type BetterAuthOptions, type BetterAuthPlugin, type Session } from "better-auth"; +import { type SecondaryStorage } from "better-auth/db"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { createAuthEndpoint, getSessionFromCtx } from "better-auth/api"; import { schema } from "./schema"; diff --git a/src/schema.ts b/src/schema.ts index c3e18a6..2bd1958 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,4 +1,4 @@ -import type { AuthPluginSchema } from "better-auth"; +import type { AuthPluginSchema } from "better-auth/db"; import type { FieldAttribute } from "better-auth/db"; import type { CloudflarePluginOptions } from "./types";