cloudflare-turnstile
This 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. Turnstile runs invisible challenges in the background, maintaining excellent user experience while blocking automated traffic. Use when: Adding bot protection to forms, implementing login security, protecting API endpoints from abuse, migrating from reCAPTCHA/hCaptcha, encountering CSP errors with Turnstile, handling token validation failures, implementing E2E tests with Turnstile, integrating with React/Next.js/Hono applications, or debugging error codes 100*, 300*, 600*. Keywords: turnstile, captcha, bot protection, cloudflare challenge, siteverify, recaptcha alternative, spam prevention, form protection, cf-turnstile, turnstile widget, token validation, managed challenge, invisible challenge, @marsidev/react-turnstile, hono turnstile, workers turnstile
What this skill does
# Cloudflare Turnstile **Status**: Production Ready **Last Updated**: 2025-10-22 **Dependencies**: None (optional: @marsidev/react-turnstile for React) **Latest Versions**: @marsidev/[email protected], [email protected] --- ## Quick Start (10 Minutes) ### 1. Create Turnstile Widget Get your sitekey and secret key from Cloudflare Dashboard. ```bash # Navigate to: https://dash.cloudflare.com/?to=/:account/turnstile # Create new widget → Copy sitekey (public) and secret key (private) ``` **Why this matters:** - Each widget has unique sitekey/secret pair - Sitekey goes in frontend (public) - Secret key ONLY in backend (private) - Use different widgets for dev/staging/production ### 2. Add Widget to Frontend Embed the Turnstile widget in your HTML form. ```html <!DOCTYPE html> <html> <head> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> </head> <body> <form id="myForm" action="/submit" method="POST"> <input type="email" name="email" required> <!-- Turnstile widget renders here --> <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div> <button type="submit">Submit</button> </form> </body> </html> ``` **CRITICAL:** - Never proxy or cache `api.js` - must load from Cloudflare CDN - Widget auto-creates hidden input `cf-turnstile-response` with token - Token expires in 5 minutes - Each token is single-use only ### 3. Validate Token on Server ALWAYS validate the token server-side. Client-side verification alone is not secure. ```typescript // Cloudflare Workers example export default { async fetch(request: Request, env: Env): Promise<Response> { const formData = await request.formData() const token = formData.get('cf-turnstile-response') const ip = request.headers.get('CF-Connecting-IP') // Validate token with Siteverify API const verifyFormData = new FormData() verifyFormData.append('secret', env.TURNSTILE_SECRET_KEY) verifyFormData.append('response', token) verifyFormData.append('remoteip', ip) const result = await fetch( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body: verifyFormData, } ) const outcome = await result.json() if (!outcome.success) { return new Response('Invalid Turnstile token', { status: 401 }) } // Token valid - proceed with form processing return new Response('Success!') } } ``` --- ## The 3-Step Setup Process ### Step 1: Create Widget Configuration 1. Log into Cloudflare Dashboard 2. Navigate to Turnstile section 3. Click "Add Site" 4. Configure: - **Widget Mode**: Managed (recommended), Non-Interactive, or Invisible - **Domains**: Add allowed hostnames (e.g., example.com, localhost for dev) - **Name**: Descriptive name (e.g., "Production Login Form") **Key Points:** - Use separate widgets for dev/staging/production - Restrict domains to only those you control - Managed mode provides best balance of security and UX - localhost must be explicitly added for local testing ### Step 2: Client-Side Integration Choose between implicit or explicit rendering: **Implicit Rendering** (Recommended for static forms): ```html <!-- 1. Load script --> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <!-- 2. Add widget --> <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-callback="onSuccess" data-error-callback="onError"></div> <script> function onSuccess(token) { console.log('Turnstile success:', token) } function onError(error) { console.error('Turnstile error:', error) } </script> ``` **Explicit Rendering** (For SPAs/dynamic UIs): ```typescript // 1. Load script with explicit mode <script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" defer></script> // 2. Render programmatically const widgetId = turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY', callback: (token) => { console.log('Token:', token) }, 'error-callback': (error) => { console.error('Error:', error) }, theme: 'auto', execution: 'render', // or 'execute' for manual trigger }) // Control lifecycle turnstile.reset(widgetId) // Reset widget turnstile.remove(widgetId) // Remove widget turnstile.execute(widgetId) // Manually trigger challenge const token = turnstile.getResponse(widgetId) // Get current token ``` **React Integration** (using @marsidev/react-turnstile): ```tsx import { Turnstile } from '@marsidev/react-turnstile' export function MyForm() { const [token, setToken] = useState<string>() return ( <form> <Turnstile siteKey={TURNSTILE_SITE_KEY} onSuccess={setToken} onError={(error) => console.error(error)} /> <button disabled={!token}>Submit</button> </form> ) } ``` ### Step 3: Server-Side Validation **MANDATORY**: Always call Siteverify API to validate tokens. ```typescript interface TurnstileResponse { success: boolean challenge_ts?: string hostname?: string error-codes?: string[] action?: string cdata?: string } async function validateTurnstile( token: string, secretKey: string, options?: { remoteip?: string idempotency_key?: string expectedAction?: string expectedHostname?: string } ): Promise<TurnstileResponse> { const formData = new FormData() formData.append('secret', secretKey) formData.append('response', token) if (options?.remoteip) { formData.append('remoteip', options.remoteip) } if (options?.idempotency_key) { formData.append('idempotency_key', options.idempotency_key) } const response = await fetch( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body: formData, } ) const result = await response.json<TurnstileResponse>() // Additional validation if (result.success) { if (options?.expectedAction && result.action !== options.expectedAction) { return { success: false, 'error-codes': ['action-mismatch'] } } if (options?.expectedHostname && result.hostname !== options.expectedHostname) { return { success: false, 'error-codes': ['hostname-mismatch'] } } } return result } // Usage in Cloudflare Worker const result = await validateTurnstile( token, env.TURNSTILE_SECRET_KEY, { remoteip: request.headers.get('CF-Connecting-IP'), expectedHostname: 'example.com', } ) if (!result.success) { return new Response('Turnstile validation failed', { status: 401 }) } ``` --- ## Critical Rules ### Always Do ✅ **Call Siteverify API** - Server-side validation is mandatory ✅ **Use HTTPS** - Never validate over HTTP ✅ **Protect secret keys** - Never expose in frontend code ✅ **Handle token expiration** - Tokens expire after 5 minutes ✅ **Implement error callbacks** - Handle failures gracefully ✅ **Use dummy keys for testing** - Test sitekey: `1x00000000000000000000AA` ✅ **Set reasonable timeouts** - Don't wait indefinitely for validation ✅ **Validate action/hostname** - Check additional fields when specified ✅ **Rotate keys periodically** - Use dashboard or API to rotate secrets ✅ **Monitor analytics** - Track solve rates and failures ### Never Do ❌ **Skip server validation** - Client-side only = security vulnerability ❌ **Proxy api.js script** - Must load from Cloudflare CDN ❌ **Reuse tokens** - Each token is single-use only ❌ **Use GET requests** - Siteverify only accepts POST ❌ **Expose secret key** - Keep secrets in backend environment only ❌ **Trust client-side validation** - Tokens can be forged ❌ **Cache api.js** - Future updates will break your integration ❌ **Use production keys in tests** - Use dummy keys instead ❌ **Ignore error callbacks** - Always handle failures --- ## Known Issues Prevention This skill prevents **12** documented issues: ### Issue #1: Missing Server-Side Validation **Error**: Zero token validation in Turnstile Analyt
Related in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.