Captcha Plugin
Protect authentication endpoints from bots with Cloudflare Turnstile captcha.
Block automated attacks and bot registrations with captcha verification. The kit uses Cloudflare Turnstile, a privacy-focused, user-friendly alternative to reCAPTCHA.
This page is part of the Authentication documentation.
Overview
The captcha plugin adds bot protection to authentication flows. When enabled, users must complete a Turnstile challenge before signing in or registering. Turnstile is designed to minimize user friction while effectively blocking bots.
Why Cloudflare Turnstile?
- Privacy-focused (no tracking cookies)
- Often invisible to users (challenges only when suspicious)
- Free tier available
- No "select all traffic lights" puzzles
Setup
1. Create Turnstile Widget
- Go to Cloudflare Dashboard
- Click Add Site
- Enter your domain (use
localhostfor development) - Select widget mode:
- Managed (recommended) - Cloudflare decides when to show challenges
- Non-interactive - Invisible verification
- Invisible - Completely hidden verification
- Copy the Site Key and Secret Key
2. Configure Environment Variables
apps/web/.env.local
TURNSTILE_SECRET_KEY=your_secret_keyNEXT_PUBLIC_CAPTCHA_SITE_KEY=your_site_key| Variable | Visibility | Description |
|---|---|---|
TURNSTILE_SECRET_KEY | Server only | Secret key for server-side verification |
NEXT_PUBLIC_CAPTCHA_SITE_KEY | Public | Site key for client-side widget |
3. Automatic Enablement
The plugin automatically enables when TURNSTILE_SECRET_KEY is set. No code changes required.
Implementation
The captcha plugin is configured in packages/better-auth/src/plugins/captcha.ts:
import * as z from 'zod';const turnstileSecretKey = z .string() .min(1) .optional() .parse(process.env.TURNSTILE_SECRET_KEY);export async function createCaptchaPlugin() { if (!turnstileSecretKey) { return [] as never; } const { captcha } = await import('better-auth/plugins'); return [ captcha({ provider: 'cloudflare-turnstile', secretKey: turnstileSecretKey, }), ];}Key behaviors:
- Returns empty array when no secret key is configured
- Uses dynamic import to avoid loading plugin when disabled
- Validates secret key with Zod
Protected Endpoints
When enabled, captcha protection applies to:
- Sign-in (
/api/auth/sign-in) - Sign-up (
/api/auth/sign-up) - Password reset (
/api/auth/forgot-password)
Development Mode
For local development, you have two options:
Option 1: Use Turnstile Test Keys
Cloudflare provides test keys that always pass:
apps/web/.env.local
# Test keys that always passTURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AANEXT_PUBLIC_CAPTCHA_SITE_KEY=1x00000000000000000000AAOption 2: Disable Captcha
Simply don't set TURNSTILE_SECRET_KEY in development. The plugin gracefully disables.
Widget Modes
Turnstile offers three widget modes:
| Mode | User Experience | Use Case |
|---|---|---|
| Managed | Shows challenge only when needed | Recommended for most apps |
| Non-interactive | Spinner, no user action | When you want visible verification |
| Invisible | Completely hidden | Maximum UX, slightly less protection |
Configure the mode in Cloudflare Dashboard when creating the widget.
Client Integration
The sign-in and sign-up forms automatically render the Turnstile widget when NEXT_PUBLIC_CAPTCHA_SITE_KEY is set. The widget token is included in auth requests.
// The auth client automatically handles captcha tokens// No manual integration needed for standard auth formsawait authClient.signIn.email({ email: 'user@example.com', password: 'password', // captcha token is automatically included});Troubleshooting
Captcha Always Fails
- Verify secret key matches site key (they're a pair)
- Check domain configuration in Cloudflare matches your app URL
- Ensure you're not using production keys on localhost
Widget Not Showing
- Verify
NEXT_PUBLIC_CAPTCHA_SITE_KEYis set - Check browser console for errors
- Ensure the Turnstile script is loading
High False Positive Rate
- Switch from Invisible to Managed mode
- Check if your users are using VPNs or Tor
- Review Turnstile analytics in Cloudflare Dashboard
Common Pitfalls
- Mismatched keys: Site key and secret key must be from the same Turnstile widget.
- Wrong domain: Development widgets must include
localhostin allowed domains. - Missing public key: Server-side works but widget won't render without
NEXT_PUBLIC_CAPTCHA_SITE_KEY. - Test keys in production: Always use real keys in production environments.
Frequently Asked Questions
Is Cloudflare Turnstile free?
Can I use reCAPTCHA instead?
Does captcha work with OAuth sign-in?
How do I test captcha locally?
Will captcha slow down sign-in?
Next: OTP Plugin →