Captcha Plugin
Bot protection with Cloudflare Turnstile
Block automated sign-up and credential stuffing attacks with Cloudflare Turnstile - add two environment variables and the captcha appears on auth forms automatically.
This page is part of the Authentication documentation.
The captcha plugin protects authentication endpoints from bots and automated attacks using Cloudflare Turnstile. When configured, sign-in, sign-up, and password reset forms require captcha verification before submitting. The plugin uses Turnstile's "invisible" mode by default - most legitimate users pass without interaction, while bots are blocked. The plugin self-disables when credentials aren't set, allowing development without captcha friction.
Cloudflare Turnstile is a privacy-focused captcha alternative that verifies users are human without puzzles, using browser signals and machine learning.
- Enable captcha when: you're launching publicly and want to prevent automated attacks, or you're seeing bot traffic on auth endpoints.
- Skip captcha when: you're in early development, running locally, or testing auth flows.
Setup
1. Create Turnstile Widget
- Go to Cloudflare Dashboard → Turnstile
- Click "Add Widget"
- Enter your domain(s)
- Choose widget mode (Managed recommended)
- Copy the Site Key and Secret Key
2. Add Environment Variables
apps/web/.env.local
TURNSTILE_SECRET_KEY=your_secret_keyNEXT_PUBLIC_CAPTCHA_SITE_KEY=your_site_key3. Verify It Works
The plugin automatically enables when credentials are present. Sign-in and sign-up forms will show the Turnstile widget. In development, you can use Cloudflare's test keys:
apps/web/.env.local (testing only)
# Always passes - for development testingTURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AANEXT_PUBLIC_CAPTCHA_SITE_KEY=1x00000000000000000000AAHow It Works
- User loads sign-in/sign-up form
- Turnstile widget renders (often invisibly)
- Turnstile analyzes browser signals
- On form submit, a token is included
- Server validates token with Cloudflare
- If valid, auth proceeds; if invalid, request is rejected
Configuration
The plugin is in packages/better-auth/src/plugins/captcha.ts:
import { captcha } from 'better-auth/plugins/captcha';export function createCaptchaPlugin() { const secretKey = process.env.TURNSTILE_SECRET_KEY; if (!secretKey) { return [] as never; // Disabled when not configured } return [ captcha({ provider: 'cloudflare-turnstile', secretKey, }), ];}Common Pitfalls
- Using production keys in development: Turnstile may block localhost. Use test keys during development.
- Mismatched domains: The Site Key is domain-specific. Add
localhostto allowed domains for development. - Forgetting the client-side key:
NEXT_PUBLIC_CAPTCHA_SITE_KEYmust be set for the widget to render. - Testing with headless browsers: Turnstile blocks headless browsers by default. Use test keys in E2E tests.
- Rate limiting on top of captcha: If you also have rate limiting, captcha failures don't count against rate limits - verify this behavior matches your security needs.
Frequently Asked Questions
Why Cloudflare Turnstile over reCAPTCHA?
Can I use reCAPTCHA instead?
Does captcha protect all endpoints?
What happens if Cloudflare is down?
How do I test captcha in E2E tests?
Next: OTP Plugin →