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 botid

Add the plugin to your auth config

auth.ts
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, 
        }, 
    }, 
});

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:

auth.ts
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 /siteverify endpoint 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 custom validateRequest) determines it's a bot, and allowed to proceed otherwise.

Plugin Options

  • provider (required): your captcha provider.
  • secretKey (required for all providers except vercel-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 set endpoints, 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 is 0.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 from botid/server in your application. Wrap it in a function if you need to pass SDK options.
  • validateRequest (optional - only Vercel BotID): custom validation logic. Return true to allow the request, false to reject it. By default, only results with isBot: false are allowed.