This application demonstrates integration of Cloudflare Turnstile CAPTCHA service with a Remix-React application.
- GitHub Repo
-
Check out the live demo
- My blog post about this
Getting Started
Prerequisites
-
Node.js 20.0.0 or higher
-
npm
-
Cloudflare account
- Turnstile widget keys provisioned
-
Cloudflare Worker (worker.js) deployed
Widget Props (v1.0)
- theme (dark || light || auto)
Demo Keys (will always verify)
-
Secret Key:
1x0000000000000000000000000000000AA
-
Public Key:
1x00000000000000000000AA
Widget Test Installation
-
Clone the repository
-
Install dependencies:
npm install
Set Variables and Deploy Cloudflare Worker
-
Insert your Turnstile public key into
keys.json
. -
Deploy
worker.js
on Cloudflare and insert the URL intokeys.json
.{ "cft_public_key": "ENTER_PUBLIC_KEY", "worker_url": "ENTER_WORKER_URL" }
-
Set your Turnstile secret key as an environment variable or secret as “CFT_SECRET_KEY” for the Worker.
Test Deployment
Note: Unless you’re using test keys, the widget will not pass the challenge or verify correctly in a local environment.
Local Dev:
npm run dev
Then run the app in production mode:
npm start
Production build for deployment:
npm run build
DIY
If you're familiar with deploying Node applications, the built-in Remix app server is production-ready.
Make sure to deploy the output of npm run build
-
build/server
-
build/client
Detailed Widget Implementation
-
Input your Cloudflare Turnstile public key into
keys.json
{ "cft_public_key": "ENTER_PUBLIC_KEY", "worker_url": "ENTER_WORKER_URL" }
-
Import the turnstile component (
turnstile.tsx
) and token verification utility (turnstile.ts
) into your relevant page. Ensure your paths are correct.import { Turnstile } from '~/turnstile/turnstile'; import { verifyTurnstileToken } from '~/utils/turnstile';
-
Create a Cloudflare Worker under your Cloudflare account.
-
In your new Worker settings, add an environment secret, your Turnstile secret key as “CFT_SECRET_KEY”.
-
In your new Worker code, copy and paste
worker.js
and deploy your worker. Make note of the worker URL, whether you’re using the default address or a custom domain. -
For increased security, change the allowed origin in the CORS headers to your domain.
const CORS_HEADERS = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', //Change this to your domain 'Access-Control-Allow-Methods': 'POST', 'Access-Control-Allow-Headers': 'Content-Type' };
-
In
keys.json
, insert your Worker URL.{ "cft_public_key": "ENTER_PUBLIC_KEY", "worker_url": "ENTER_WORKER_URL" }
-
Render the Turnstile widget in your form. Retain the success prop if you want the widget to be removed after successful verification.
return ( /* YOUR FORM CODE ...*/ <Turnstile theme="dark" success={status === 'success'} {/* OTHER CUSTOM PROPS */} /> /* YOUR FORM CODE ... */
-
Ensure you include server-side token verification using
verifyTurnstileToken
.Do not modify “cf-turnstile-response” - This is the token response from Cloudflare required for token verification.
/* TOKEN VERIFICATION SHOULD BE FIRST ACTION UPON SUBMIT */ const token = formData.get('cf-turnstile-response') as string; try { const result = await verifyTurnstileToken(token); if ('success' in result && result.success) { /* ENTER SUCCESS LOGIC */ setStatus('success'); } /* ENTER FAILURE LOGIC */ else { setStatus('error'); if ('message' in result) { setErrorMessage(result.message || 'Verification failed'); } else { setErrorMessage('Verification failed'); } } } catch (error) { setStatus('error'); setErrorMessage('Verification failed'); } /* OTHER FORM LOGIC/SUBMISSION LOGIC... */
Questions/Issues
I welcome pull requests and issues. You can also contact me directly.