$npx -y skills add jackspace/ClaudeSkillz --skill cloudflare-turnstileThis skill provides comprehensive knowledge for implementing Cloudflare Turnstile, the CAPTCHA-alternative bot protection system. It should be used when integrating bot protection into forms, login pages, signup flows, or any user-facing feature requiring spam/bot prevention. Tur
| 1 | # Cloudflare Turnstile |
| 2 | |
| 3 | **Status**: Production Ready |
| 4 | **Last Updated**: 2025-10-22 |
| 5 | **Dependencies**: None (optional: @marsidev/react-turnstile for React) |
| 6 | **Latest Versions**: @marsidev/react-turnstile@1.3.1, turnstile-types@1.2.3 |
| 7 | |
| 8 | --- |
| 9 | |
| 10 | ## Quick Start (10 Minutes) |
| 11 | |
| 12 | ### 1. Create Turnstile Widget |
| 13 | |
| 14 | Get your sitekey and secret key from Cloudflare Dashboard. |
| 15 | |
| 16 | ```bash |
| 17 | # Navigate to: https://dash.cloudflare.com/?to=/:account/turnstile |
| 18 | # Create new widget → Copy sitekey (public) and secret key (private) |
| 19 | ``` |
| 20 | |
| 21 | **Why this matters:** |
| 22 | - Each widget has unique sitekey/secret pair |
| 23 | - Sitekey goes in frontend (public) |
| 24 | - Secret key ONLY in backend (private) |
| 25 | - Use different widgets for dev/staging/production |
| 26 | |
| 27 | ### 2. Add Widget to Frontend |
| 28 | |
| 29 | Embed the Turnstile widget in your HTML form. |
| 30 | |
| 31 | ```html |
| 32 | <!DOCTYPE html> |
| 33 | <html> |
| 34 | <head> |
| 35 | <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> |
| 36 | </head> |
| 37 | <body> |
| 38 | <form id="myForm" action="/submit" method="POST"> |
| 39 | <input type="email" name="email" required> |
| 40 | <!-- Turnstile widget renders here --> |
| 41 | <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div> |
| 42 | <button type="submit">Submit</button> |
| 43 | </form> |
| 44 | </body> |
| 45 | </html> |
| 46 | ``` |
| 47 | |
| 48 | **CRITICAL:** |
| 49 | - Never proxy or cache `api.js` - must load from Cloudflare CDN |
| 50 | - Widget auto-creates hidden input `cf-turnstile-response` with token |
| 51 | - Token expires in 5 minutes |
| 52 | - Each token is single-use only |
| 53 | |
| 54 | ### 3. Validate Token on Server |
| 55 | |
| 56 | ALWAYS validate the token server-side. Client-side verification alone is not secure. |
| 57 | |
| 58 | ```typescript |
| 59 | // Cloudflare Workers example |
| 60 | export default { |
| 61 | async fetch(request: Request, env: Env): Promise<Response> { |
| 62 | const formData = await request.formData() |
| 63 | const token = formData.get('cf-turnstile-response') |
| 64 | const ip = request.headers.get('CF-Connecting-IP') |
| 65 | |
| 66 | // Validate token with Siteverify API |
| 67 | const verifyFormData = new FormData() |
| 68 | verifyFormData.append('secret', env.TURNSTILE_SECRET_KEY) |
| 69 | verifyFormData.append('response', token) |
| 70 | verifyFormData.append('remoteip', ip) |
| 71 | |
| 72 | const result = await fetch( |
| 73 | 'https://challenges.cloudflare.com/turnstile/v0/siteverify', |
| 74 | { |
| 75 | method: 'POST', |
| 76 | body: verifyFormData, |
| 77 | } |
| 78 | ) |
| 79 | |
| 80 | const outcome = await result.json() |
| 81 | |
| 82 | if (!outcome.success) { |
| 83 | return new Response('Invalid Turnstile token', { status: 401 }) |
| 84 | } |
| 85 | |
| 86 | // Token valid - proceed with form processing |
| 87 | return new Response('Success!') |
| 88 | } |
| 89 | } |
| 90 | ``` |
| 91 | |
| 92 | --- |
| 93 | |
| 94 | ## The 3-Step Setup Process |
| 95 | |
| 96 | ### Step 1: Create Widget Configuration |
| 97 | |
| 98 | 1. Log into Cloudflare Dashboard |
| 99 | 2. Navigate to Turnstile section |
| 100 | 3. Click "Add Site" |
| 101 | 4. Configure: |
| 102 | - **Widget Mode**: Managed (recommended), Non-Interactive, or Invisible |
| 103 | - **Domains**: Add allowed hostnames (e.g., example.com, localhost for dev) |
| 104 | - **Name**: Descriptive name (e.g., "Production Login Form") |
| 105 | |
| 106 | **Key Points:** |
| 107 | - Use separate widgets for dev/staging/production |
| 108 | - Restrict domains to only those you control |
| 109 | - Managed mode provides best balance of security and UX |
| 110 | - localhost must be explicitly added for local testing |
| 111 | |
| 112 | ### Step 2: Client-Side Integration |
| 113 | |
| 114 | Choose between implicit or explicit rendering: |
| 115 | |
| 116 | **Implicit Rendering** (Recommended for static forms): |
| 117 | ```html |
| 118 | <!-- 1. Load script --> |
| 119 | <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> |
| 120 | |
| 121 | <!-- 2. Add widget --> |
| 122 | <div class="cf-turnstile" |
| 123 | data-sitekey="YOUR_SITE_KEY" |
| 124 | data-callback="onSuccess" |
| 125 | data-error-callback="onError"></div> |
| 126 | |
| 127 | <script> |
| 128 | function onSuccess(token) { |
| 129 | console.log('Turnstile success:', token) |
| 130 | } |
| 131 | |
| 132 | function onError(error) { |
| 133 | console.error('Turnstile error:', error) |
| 134 | } |
| 135 | </script> |
| 136 | ``` |
| 137 | |
| 138 | **Explicit Rendering** (For SPAs/dynamic UIs): |
| 139 | ```typescript |
| 140 | // 1. Load script with explicit mode |
| 141 | <script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" defer></script> |
| 142 | |
| 143 | // 2. Render |