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.
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.
// 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
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.