Cloudflare Turnstile
Invisible bot protection for public-facing forms without traditional CAPTCHAs.
Overview
PROIGN uses Cloudflare Turnstile to protect public forms from bots and automated abuse. Unlike traditional CAPTCHAs, Turnstile is invisible to most users — no puzzles to solve.
Where Turnstile Is Used
- Registration — New tenant sign-up form
- Contact — Public contact form with file attachments
- Support Portal — Public ticket submission
- QR Redirect — Rate-limited QR code scans
How It Works
- The Turnstile widget loads invisibly when a protected form renders
- Turnstile analyzes browser signals to determine if the visitor is human
- On form submit, a challenge token is included in the request
- The server-side Worker validates the token against the Turnstile API before processing
- If validation fails, the request is rejected with a 403 error
Server-Side Validation
POST https://challenges.cloudflare.com/turnstile/v0/siteverify
{
"secret": "<TURNSTILE_SECRET_KEY>",
"response": "<token-from-client>",
"remoteip": "<user-ip>" // optional
}
// Success response
{
"success": true,
"challenge_ts": "2026-02-21T09:00:00.000Z",
"hostname": "www.proign.com"
}Widget Modes
Turnstile supports multiple widget modes depending on the form:
| Mode | Behavior | Used In |
|---|---|---|
managed | Shows checkbox only if needed | Registration, Contact |
non-interactive | Validates silently, no user interaction | Support Portal |
invisible | Completely hidden, triggered on submit | QR Redirect |
Error Handling
When Turnstile validation fails, the API returns a structured error:
// Failed validation response (403 Forbidden)
{
"error": "Turnstile verification failed. Please try again."
}
// Common failure reasons:
// - Token expired (tokens are valid for 300 seconds)
// - Token already used (each token is single-use)
// - Invalid or missing token
// - Secret key mismatchOn the client side, if Turnstile fails to load (e.g., ad blocker), the form falls back to allowing submission without a token. The server then applies stricter rate limiting to compensate.
Configuration
Turnstile requires two keys stored as Cloudflare Worker secrets:
| Secret | Used By |
|---|---|
TURNSTILE_SITE_KEY | Client-side widget (public) |
TURNSTILE_SECRET_KEY | Server-side validation (secret) |
Testing Keys
Cloudflare provides test keys for development:
| Key | Behavior |
|---|---|
1x00000000000000000000AA | Always passes |
2x00000000000000000000AB | Always fails |
Client-Side Integration
Turnstile is embedded in forms using a React component that loads the Cloudflare script and renders the widget:
// Simplified Turnstile component usage
<form onSubmit={handleSubmit}>
<input type="email" name="email" required />
<textarea name="message" required />
{/* Turnstile widget renders here */}
<Turnstile
siteKey={TURNSTILE_SITE_KEY}
onSuccess={(token) => setTurnstileToken(token)}
onError={() => setTurnstileToken(null)}
/>
<button type="submit" disabled={!turnstileToken}>
Submit
</button>
</form>
// On form submit, include the token
const handleSubmit = async (e) => {
const response = await fetch("/api/contact", {
method: "POST",
body: JSON.stringify({
email,
message,
turnstileToken // Server validates this
})
});
};Fallback Behavior
PROIGN handles Turnstile failures gracefully:
| Scenario | Behavior |
|---|---|
| Turnstile loads normally | Token required for form submission |
| Ad blocker blocks Turnstile | Form submits without token, server applies 1 req/min rate limit |
| Token expired (300s) | Widget auto-refreshes, user may need to resubmit |
| Cloudflare outage | Same as ad blocker — graceful degradation with rate limiting |