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

  1. Go to Cloudflare Dashboard
  2. Click Add Site
  3. Enter your domain (use localhost for development)
  4. Select widget mode:
    • Managed (recommended) - Cloudflare decides when to show challenges
    • Non-interactive - Invisible verification
    • Invisible - Completely hidden verification
  5. Copy the Site Key and Secret Key

2. Configure Environment Variables

apps/web/.env.local

TURNSTILE_SECRET_KEY=your_secret_key
NEXT_PUBLIC_CAPTCHA_SITE_KEY=your_site_key
VariableVisibilityDescription
TURNSTILE_SECRET_KEYServer onlySecret key for server-side verification
NEXT_PUBLIC_CAPTCHA_SITE_KEYPublicSite 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 pass
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
NEXT_PUBLIC_CAPTCHA_SITE_KEY=1x00000000000000000000AA

Option 2: Disable Captcha

Simply don't set TURNSTILE_SECRET_KEY in development. The plugin gracefully disables.

Widget Modes

Turnstile offers three widget modes:

ModeUser ExperienceUse Case
ManagedShows challenge only when neededRecommended for most apps
Non-interactiveSpinner, no user actionWhen you want visible verification
InvisibleCompletely hiddenMaximum 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 forms
await authClient.signIn.email({
email: 'user@example.com',
password: 'password',
// captcha token is automatically included
});

Troubleshooting

Captcha Always Fails

  1. Verify secret key matches site key (they're a pair)
  2. Check domain configuration in Cloudflare matches your app URL
  3. Ensure you're not using production keys on localhost

Widget Not Showing

  1. Verify NEXT_PUBLIC_CAPTCHA_SITE_KEY is set
  2. Check browser console for errors
  3. Ensure the Turnstile script is loading

High False Positive Rate

  1. Switch from Invisible to Managed mode
  2. Check if your users are using VPNs or Tor
  3. 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 localhost in 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?
Yes. Turnstile has a generous free tier. Paid tiers offer additional features like custom branding and higher limits.
Can I use reCAPTCHA instead?
Better Auth primarily supports Turnstile. Using reCAPTCHA would require custom implementation or a different plugin.
Does captcha work with OAuth sign-in?
OAuth authentication bypasses the captcha since users are authenticated through the OAuth provider's own security measures.
How do I test captcha locally?
Use Cloudflare's test keys that always pass, or simply don't configure the secret key to disable captcha in development.
Will captcha slow down sign-in?
Minimally. Managed mode only shows challenges for suspicious traffic. Most legitimate users see no visible captcha.

Next: OTP Plugin →