Skip to main content

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

  1. The Turnstile widget loads invisibly when a protected form renders
  2. Turnstile analyzes browser signals to determine if the visitor is human
  3. On form submit, a challenge token is included in the request
  4. The server-side Worker validates the token against the Turnstile API before processing
  5. 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:

ModeBehaviorUsed In
managedShows checkbox only if neededRegistration, Contact
non-interactiveValidates silently, no user interactionSupport Portal
invisibleCompletely hidden, triggered on submitQR 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 mismatch

On 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:

SecretUsed By
TURNSTILE_SITE_KEYClient-side widget (public)
TURNSTILE_SECRET_KEYServer-side validation (secret)

Testing Keys

Cloudflare provides test keys for development:

KeyBehavior
1x00000000000000000000AAAlways passes
2x00000000000000000000ABAlways 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:

ScenarioBehavior
Turnstile loads normallyToken required for form submission
Ad blocker blocks TurnstileForm submits without token, server applies 1 req/min rate limit
Token expired (300s)Widget auto-refreshes, user may need to resubmit
Cloudflare outageSame as ad blocker — graceful degradation with rate limiting

Related