pealdevDOCS
Back to peal.dev

AUTH STARTER · CAPTCHA (TURNSTILE)

Captcha (Turnstile)

Invisible Cloudflare Turnstile on every auth endpoint. Auto-enabled when the env vars are set. Zero user friction.

We use Cloudflare Turnstile because it's free, doesn't ship user data to Google, and runs in interaction-only mode (no "click all the crosswalks"). When the keys are set, captcha kicks in on every auth call automatically.

Get the keys

https://dash.cloudflare.com/?to=/:account/turnstile → add a site → copy site key and secret. Free.

Terminal
TURNSTILE_SECRET_KEY="0x4AAA..."
NEXT_PUBLIC_TURNSTILE_SITE_KEY="0x4AAA..."

That's it — no code change

The captcha plugin's enabled is derived from NEXT_PUBLIC_TURNSTILE_SITE_KEY being set. The components already render the widget and pass the token in fetch headers via the use-captcha hook.

TypeScript
// src/auth.config.ts
plugins: {
  captcha: {
    enabled: Boolean(process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY),
    siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ?? "",
  },
},

How it's wired in forms

TypeScript
const { token: captchaToken, captchaHeaders, setCaptchaToken } = useCaptcha();

<Captcha onVerify={setCaptchaToken} />

await authClient.signIn.email({
  email,
  password,
  fetchOptions: { headers: captchaHeaders },
});

Why invisible mode is the right default

Turnstile in interaction-only mode runs silently for 99% of users. Only suspicious traffic gets a challenge. Your sign-in form looks unchanged; bots get filtered. Best UX/protection tradeoff we've found.

Add your prod domain to Turnstile's allowed hostnames

By default Turnstile only allows the hostname you added when creating the site. If you forget, captcha silently fails in prod with a CORS-ish error. Add yourdomain.com and *.yourdomain.com in the Turnstile dashboard.