diff --git a/docs/content/docs/concepts/database.mdx b/docs/content/docs/concepts/database.mdx
index 8c7e0ec9db..78eca210fc 100644
--- a/docs/content/docs/concepts/database.mdx
+++ b/docs/content/docs/concepts/database.mdx
@@ -124,7 +124,7 @@ If you're using Cloudflare D1 with Drizzle or Prisma, use [`cloudflare:workers`]
## Secondary Storage
-Secondary storage in Better Auth allows you to use key-value stores for managing session data, rate limiting counters, etc. This can be useful when you want to offload the storage of intensive records to a high performance storage or even RAM.
+Secondary storage in Better Auth allows you to use key-value stores for managing session data, verification records, rate limiting counters, and other short-lived auth data. This can be useful when you want to offload the storage of intensive records to a high performance storage or even RAM.
### Implementation
diff --git a/docs/content/docs/plugins/2fa.mdx b/docs/content/docs/plugins/2fa.mdx
index b03c655644..dab2dfb073 100644
--- a/docs/content/docs/plugins/2fa.mdx
+++ b/docs/content/docs/plugins/2fa.mdx
@@ -532,7 +532,7 @@ these are options for OTP.
default: 3,
},
storeOTP: {
- description: "How to store the otp in the database. Whether to store it as plain text, encrypted or hashed. You can also provide a custom encryptor or hasher.",
+ description: "How to transform the stored OTP value, whether plain text, encrypted, or hashed. You can also provide a custom encryptor or hasher. The storage backend is controlled by the global verification config, so secondary storage can be used instead of the database.",
type: "string",
default: "plain",
},
diff --git a/docs/content/docs/plugins/email-otp.mdx b/docs/content/docs/plugins/email-otp.mdx
index 6a93e2f5e1..aa84cd5c74 100644
--- a/docs/content/docs/plugins/email-otp.mdx
+++ b/docs/content/docs/plugins/email-otp.mdx
@@ -411,13 +411,13 @@ export const auth = betterAuth({
When the maximum attempts are exceeded, the `verifyOTP`, `signIn.emailOtp`, `verifyEmail`, and `resetPassword` methods will return an error with code `TOO_MANY_ATTEMPTS`.
-- `storeOTP`: The method to store the OTP in your database, whether `encrypted`, `hashed` or `plain` text. Default is `plain` text.
+- `storeOTP`: The method used to transform the OTP before it is stored by Better Auth's verification layer, whether `encrypted`, `hashed` or `plain` text. Default is `plain` text.
-Note: This will not affect the OTP sent to the user, it will only affect the OTP stored in your database.
+Note: This will not affect the OTP sent to the user. It only affects the stored OTP value. The storage backend itself is controlled by the global [`verification`](/docs/reference/options#verification) config, so if you configure `secondaryStorage`, these verification records can live there instead of the database.
-Alternatively, you can pass a custom encryptor or hasher to store the OTP in your database.
+Alternatively, you can pass a custom encryptor or hasher to control how the stored OTP value is persisted.
**Custom encryptor**
diff --git a/docs/content/docs/plugins/magic-link.mdx b/docs/content/docs/plugins/magic-link.mdx
index 16b2146aa5..40c59e342e 100644
--- a/docs/content/docs/plugins/magic-link.mdx
+++ b/docs/content/docs/plugins/magic-link.mdx
@@ -149,10 +149,12 @@ and a `ctx` context object as the second parameter.
default, we return a long and cryptographically secure string.
-**storeToken**: The `storeToken` function is called to store the magic link token in the database. The default value is `"plain"`.
+**storeToken**: The `storeToken` function controls how the magic link token is transformed before it is stored by Better Auth's verification layer. The default value is `"plain"`.
The `storeToken` function can be one of the following:
- `"plain"`: The token is stored in plain text.
- `"hashed"`: The token is hashed using the default hasher.
- `{ type: "custom-hasher", hash: (token: string) => Promise }`: The token is hashed using a custom hasher.
+
+The storage backend itself is controlled by the global [`verification`](/docs/reference/options#verification) config. If you configure `secondaryStorage`, magic link verification records can be stored there instead of the database.
diff --git a/docs/content/docs/reference/options.mdx b/docs/content/docs/reference/options.mdx
index 75960d851d..93b203d911 100644
--- a/docs/content/docs/reference/options.mdx
+++ b/docs/content/docs/reference/options.mdx
@@ -192,7 +192,7 @@ Read more about databases [here](/docs/concepts/database).
## `secondaryStorage`
-Secondary storage configuration used to store session and rate limit data.
+Secondary storage configuration used to store session data, verification records, and rate limit data.
```ts
import { betterAuth } from "better-auth";
@@ -491,12 +491,13 @@ Verification configuration options.
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
+ secondaryStorage: {
+ // your Redis or KV implementation
+ },
verification: {
- modelName: "verifications",
- fields: {
- userId: "user_id"
- },
- disableCleanup: false
+ disableCleanup: false,
+ storeIdentifier: "hashed",
+ storeInDatabase: false
},
})
```
@@ -504,6 +505,10 @@ export const auth = betterAuth({
- `modelName`: The model name for the verification table
- `fields`: Map fields to different column names
- `disableCleanup`: Disable cleaning up expired values when a verification value is fetched
+- `storeIdentifier`: How to store verification identifiers such as tokens and OTP keys. Supports `"plain"`, `"hashed"`, or a custom hasher. You can also use `{ default, overrides }` to apply different strategies per identifier prefix.
+- `storeInDatabase`: Store verification records in the database even when `secondaryStorage` is configured. Default is `false`.
+
+If `secondaryStorage` is configured, verification records are stored there by default. That behavior applies to flows that use Better Auth's shared verification layer, including OTP and magic-link style flows.
## `rateLimit`