🎯

cloudflare-turnstile

🎯Skill

from ovachiever/droid-tings

VibeIndex|
What it does

Validates and protects web forms using Cloudflare Turnstile, offering a modern, privacy-friendly alternative to traditional CAPTCHAs with easy integration across web frameworks.

πŸ“¦

Part of

ovachiever/droid-tings(370 items)

cloudflare-turnstile

Installation

git cloneClone repository
git clone https://github.com/ovachiever/droid-tings.git
πŸ“– Extracted from docs: ovachiever/droid-tings
14Installs
20
-
AddedFeb 4, 2026

Skill Details

SKILL.md

|

Overview

# Cloudflare Turnstile

Status: Production Ready

Last Updated: 2025-10-22

Dependencies: None (optional: @marsidev/react-turnstile for React)

Latest Versions: @marsidev/react-turnstile@1.3.1, turnstile-types@1.2.3

---

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

```

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 {

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

data-sitekey="YOUR_SITE_KEY"

data-callback="onSuccess"

data-error-callback="onError">

```

Explicit Rendering (For SPAs/dynamic UIs):

```typescript

// 1. Load script with explicit mode

// 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()

return (

siteKey={TURNSTILE_SITE_KEY}

onSuccess={setToken}

onError={(error) => console.error(error)}

/>

)

}

```

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 {

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()

// 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 Analytics dashboard

Source: https://developers.cloudflare.com/turnstile/get-started/

Why It Happens: Developers only implement client-side widget, skip Siteverify call

Prevention: All templates include mandatory server-side validation with Siteverify API

Issue #2: Token Expiration (5 Minutes)

Error: success: false for valid tokens submitted after delay

Source: https://developers.cloudflare.com/turnstile/get-started/server-side-validation

Why It Happens: Tokens expire 300 seconds after generation

Prevention: Templates document TTL and implement token refresh on expiration

Issue #3: Secret Key Exposed in Frontend

Error: Security bypass - attackers can validate their own tokens

Source: https://developers.cloudflare.com/turnstile/get-started/server-side-validation

Why It Happens: Secret key hardcoded in JavaScript or visible in source

Prevention: All templates show backend-only validation with environment variables

Issue #4: GET Request to Siteverify

Error: API returns 405 Method Not Allowed

Source: https://developers.cloudflare.com/turnstile/migration/recaptcha

Why It Happens: reCAPTCHA supports GET, Turnstile requires POST

Prevention: Templates use POST with FormData or JSON body

Issue #5: Content Security Policy Blocking

Error: Error 200500 - "Loading error: The iframe could not be loaded"

Source: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes

Why It Happens: CSP blocks challenges.cloudflare.com iframe

Prevention: Skill includes CSP configuration reference and check-csp.sh script

Issue #6: Widget Crash (Error 300030)

Error: Generic client execution error for legitimate users

Source: https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903

Why It Happens: Unknown - appears to be Cloudflare-side issue (2025)

Prevention: Templates implement error callbacks, retry logic, and fallback handling

Issue #7: Configuration Error (Error 600010)

Error: Widget fails with "configuration error"

Source: https://community.cloudflare.com/t/repeated-cloudflare-turnstile-error-600010/644578

Why It Happens: Missing or deleted hostname in widget configuration

Prevention: Templates document hostname allowlist requirement and verification steps

Issue #8: Safari 18 / macOS 15 "Hide IP" Issue

Error: Error 300010 when Safari's "Hide IP address" is enabled

Source: https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903

Why It Happens: Privacy settings interfere with challenge signals

Prevention: Error handling reference documents Safari workaround (disable Hide IP)

Issue #9: Brave Browser Confetti Animation Failure

Error: Verification fails during success animation

Source: https://github.com/brave/brave-browser/issues/45608 (April 2025)

Why It Happens: Brave shields block animation scripts

Prevention: Templates handle success before animation completes

Issue #10: Next.js + Jest Incompatibility

Error: @marsidev/react-turnstile breaks Jest tests

Source: https://github.com/marsidev/react-turnstile/issues/112 (Oct 2025)

Why It Happens: Module resolution issues with Jest

Prevention: Testing guide includes Jest mocking patterns and dummy sitekey usage

Issue #11: localhost Not in Allowlist

Error: Error 110200 - "Unknown domain: Domain not allowed"

Source: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes

Why It Happens: Production widget used in development without localhost in allowlist

Prevention: Templates use dummy test keys for dev, document localhost allowlist requirement

Issue #12: Token Reuse Attempt

Error: success: false with "token already spent" error

Source: https://developers.cloudflare.com/turnstile/troubleshooting/testing

Why It Happens: Each token can only be validated once

Prevention: Templates document single-use constraint and token refresh patterns

---

Configuration Files Reference

wrangler.jsonc (Cloudflare Workers)

```jsonc

{

"name": "my-app",

"main": "src/index.ts",

"compatibility_date": "2025-10-22",

// Public sitekey (safe to commit)

"vars": {

"TURNSTILE_SITE_KEY": "1x00000000000000000000AA" // Use real key in production

},

// Secret key (DO NOT commit - use wrangler secret)

// Run: wrangler secret put TURNSTILE_SECRET_KEY

"secrets": ["TURNSTILE_SECRET_KEY"]

}

```

Why these settings:

  • vars for public sitekey (visible in client code)
  • secrets for private secret key (encrypted, backend-only)
  • Use dummy keys for development (see testing-guide.md)
  • Rotate production secret keys quarterly

Required CSP Directives

```html

script-src 'self' https://challenges.cloudflare.com;

frame-src 'self' https://challenges.cloudflare.com;

connect-src 'self' https://challenges.cloudflare.com;

">

```

---

Common Patterns

Pattern 1: Hono + Cloudflare Workers

```typescript

import { Hono } from 'hono'

type Bindings = {

TURNSTILE_SECRET_KEY: string

TURNSTILE_SITE_KEY: string

}

const app = new Hono<{ Bindings: Bindings }>()

app.post('/api/login', async (c) => {

const body = await c.req.formData()

const token = body.get('cf-turnstile-response')

if (!token) {

return c.text('Missing Turnstile token', 400)

}

// Validate token

const verifyFormData = new FormData()

verifyFormData.append('secret', c.env.TURNSTILE_SECRET_KEY)

verifyFormData.append('response', token.toString())

verifyFormData.append('remoteip', c.req.header('CF-Connecting-IP') || '')

const verifyResult = await fetch(

'https://challenges.cloudflare.com/turnstile/v0/siteverify',

{

method: 'POST',

body: verifyFormData,

}

)

const outcome = await verifyResult.json<{ success: boolean }>()

if (!outcome.success) {

return c.text('Invalid Turnstile token', 401)

}

// Process login

return c.json({ message: 'Login successful' })

})

export default app

```

When to use: API routes in Cloudflare Workers with Hono framework

Pattern 2: React + Next.js App Router

```tsx

'use client'

import { Turnstile } from '@marsidev/react-turnstile'

import { useState } from 'react'

export function ContactForm() {

const [token, setToken] = useState()

const [error, setError] = useState()

async function handleSubmit(e: React.FormEvent) {

e.preventDefault()

if (!token) {

setError('Please complete the challenge')

return

}

const formData = new FormData(e.currentTarget)

formData.append('cf-turnstile-response', token)

const response = await fetch('/api/contact', {

method: 'POST',

body: formData,

})

if (!response.ok) {

setError('Submission failed')

return

}

// Success

}

return (