Platform
Cloudflare Turnstile
Bot protection on the hosted pages, validated on the server.
Where it applies#
| Page | Checked submissions |
|---|---|
| Hosted AuthKit pages on 3een subdomains | Password sign-in, password sign-up, forgot password, sending a magic code |
| 3een’s own sign-in pages | Password sign-in, sign-up, forgot password, resending a verification code |
Follow-up steps are not checked again: an MFA code, a reset with its emailed link, a verification code or the return from a social provider already carry their own proof. The widget runs invisibly and asks for a click only when Cloudflare is unsure.
Server-side validation#
- The token is verified with Cloudflare's Siteverify API on the server before anything reaches the authentication API.
- The token is removed from the request before it is forwarded. The API never sees it.
- The visitor's IP is read from the right-hand end of
X-Forwarded-For, the entry the trusted proxy added, never from a value the client supplied. - The secret key stays on the server; the page only ever has the public site key.
Failures#
A failed or missing check is 403 with { code: "bot_check_failed" }, and the page offers Try again. If Cloudflare can't be reached, submissions are refused rather than let through (fail closed).
Self-hosting the pages#
If you run the authentication frontend yourself, Turnstile is configured with two values, set together:
| Variable | Where | Notes |
|---|---|---|
NEXT_PUBLIC_TURNSTILE_SITE_KEY | Build time (public) | Renders the widget. |
TURNSTILE_SECRET_KEY | Runtime, server only | Verifies tokens. |
TRUSTED_PROXY_HOPS | Runtime | Proxies that append to X-Forwarded-For: 1 behind Cloud Run alone, 2 behind an external load balancer. |
Both unset turns the check off. Only one set is a configuration error: in production, protected submissions are refused (503) rather than run unprotected. For testing, Cloudflare publishes test keys that always pass, always fail, or always ask for a click.