Captcha
Captcha plugin
The Captcha Plugin integrates bot protection into your Better Auth system by adding captcha verification for key endpoints. This plugin ensures that only human users can perform actions like signing up, signing in, or resetting passwords. The following providers are currently supported:
The default endpoints cover Email & Password authentication. Other authentication methods need an explicit endpoints array. Vercel BotID also needs the client-side setup below.
Installation
Install dependencies (Optional)
If you are using Vercel BotID, install its SDK in your application:
npm install botidAdd the plugin to your auth config
import { betterAuth } from "better-auth";
import { captcha } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
captcha({
provider: "cloudflare-turnstile", // or google-recaptcha, hcaptcha, captchafox
secretKey: process.env.TURNSTILE_SECRET_KEY!,
}),
],
});Send the token for token-based providers
The x-captcha-user-remote-ip header is no longer required—IP is now auto-detected server-side.
For token-based providers, add the captcha token to requests for protected endpoints. This example shows a signIn request:
import { authClient } from "@/lib/auth-client"
await authClient.signIn.email({
email: "user@example.com",
password: "secure-password",
fetchOptions: {
headers: {
"x-captcha-response": turnstileToken,
},
},
});- To implement Cloudflare Turnstile on the client side, follow the official Cloudflare Turnstile documentation or use a library like react-turnstile.
- To implement Google reCAPTCHA on the client side, follow the official Google reCAPTCHA documentation or use libraries like react-google-recaptcha (v2) and react-google-recaptcha-v3 (v3).
- To implement hCaptcha on the client side, follow the official hCaptcha documentation or use libraries like @hcaptcha/react-hcaptcha
- To implement CaptchaFox on the client side, follow the official CaptchaFox documentation or use libraries like @captchafox/react
Providers
Vercel BotID
BotID requires a Vercel deployment and client-side setup. Follow Vercel's setup guide for the rewrites and client integration appropriate to your framework. The Better Auth plugin does not use a secretKey or an x-captcha-response header:
import { checkBotId } from "botid/server";
import { betterAuth } from "better-auth";
import { captcha } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
captcha({
provider: "vercel-botid",
checkBotId,
}),
],
});When using initBotId, protect Better Auth's default auth routes at their actual request paths:
import { initBotId } from "botid/client/core";
initBotId({
protect: [
{ path: "/api/auth/sign-up/email", method: "POST" },
{ path: "/api/auth/sign-in/email", method: "POST" },
{ path: "/api/auth/request-password-reset", method: "POST" },
],
});BotID uses the browser's full request path. The plugin's endpoints option omits the Better Auth base path, as in /sign-in/email. Keep both in sync if you change basePath or endpoints.
With BotID initialized in the browser, sign in normally without a captcha token header:
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.signIn.email({
email: "user@example.com",
password: "secure-password",
});BotID does not support native HTML form submissions.
Local development allows requests by default, so verify the full flow on Vercel before relying on it in production. For route-specific detection levels, follow Vercel's advanced configuration.
How it works
The plugin acts as a middleware: it intercepts requests to configured endpoints (see endpoints
in the Plugin Options section).
it validates the captcha token on the server, by calling the captcha provider's /siteverify.
Vercel BotID is the exception: it uses the supplied checkBotId function instead of a captcha token or /siteverify call.
- if the token is missing, gets rejected by the captcha provider, or if the
/siteverifyendpoint is unavailable, the plugin returns an error and interrupts the request. - if the token is accepted by the captcha provider, the middleware returns
undefined, meaning the request is allowed to proceed. - for Vercel BotID, the request is rejected when
checkBotId(or your customvalidateRequest) determines it's a bot, and allowed to proceed otherwise.
Plugin Options
provider(required): your captcha provider.secretKey(required for all providers exceptvercel-botid): your provider's secret key used for the server-side validation.endpoints(optional): Choose which paths require CAPTCHA. By default, CAPTCHA protects/sign-up/email,/sign-in/email, and/request-password-reset. Requests to other paths don't require CAPTCHA. When you setendpoints, include every path you want to protect because it replaces the defaults. You can use exact paths or wildcard patterns like/sign-in/*for one segment and/sign-in/**for nested routes.minScore(optional - only Google ReCAPTCHA v3): minimum score threshold. Default is0.5.siteKey(optional - only hCaptcha and CaptchaFox): prevents tokens issued on one sitekey from being redeemed elsewhere.siteVerifyURLOverride(optional - token-based providers only): overrides the captcha verification URL.checkBotId(required for Vercel BotID): Vercel's server-side check function, imported frombotid/serverin your application. Wrap it in a function if you need to pass SDK options.validateRequest(optional - only Vercel BotID): custom validation logic. Returntrueto allow the request,falseto reject it. By default, only results withisBot: falseare allowed.