wappler-cf-turnstile-server
v1.0.1
Published
Cloudflare Turnstile server-side verification module for Wappler Server Connect - Verify CAPTCHA tokens on the backend. Companion to wappler-cf-turnstile-client.
Maintainers
Readme
Cloudflare Turnstile Server-Side Verification
Server Connect module for Wappler that verifies Cloudflare Turnstile CAPTCHA tokens on the backend.
Description
This is the server-side companion to wappler-cf-turnstile-client. Use this module to verify Turnstile tokens received from the client and confirm that users are legitimate.
Features
- Verify Turnstile tokens with Cloudflare API
- Built-in error handling
- Support for optional parameters (IP address, idempotency key)
- Returns complete verification response
- No external dependencies (uses Node.js built-in modules)
- Timeout protection (10 seconds)
- Detailed logging
Installation
Install this extension directly from Wappler:
- Open your Wappler project
- Go to Project Settings → Extensions
- Click Add Extension
- Search for
wappler-cf-turnstile-server - Click Install
- Restart Wappler
The "Verify Turnstile" action will appear in Server Connect under "Cloudflare Turnstile".
Usage
1. Get Your Secret Key
Get your Turnstile secret key from: https://dash.cloudflare.com/
Store it securely in environment variables:
# .env
TURNSTILE_SECRET_KEY=your-secret-key-here2. Add to Server Connect
In Wappler Server Connect, you'll find the action under:
Cloudflare Turnstile > Verify Turnstile
3. Configure the Action
Required Parameters:
- Secret Key: Your Cloudflare secret key
- Turnstile Token: The token from the client
Optional Parameters:
- Remote IP: User's IP address
- Idempotency Key: Unique key to prevent duplicate verifications
Example Server Connect Action
{
"meta": {
"options": {
"csrf": false
}
},
"exec": {
"steps": [
{
"name": "verifyTurnstile",
"module": "turnstile",
"action": "verify",
"options": {
"secret": "{{$_ENV.TURNSTILE_SECRET_KEY}}",
"response": "{{$_POST['cf-turnstile-response']}}",
"remoteip": "{{$_SERVER.REMOTE_ADDR}}"
},
"output": true
},
{
"name": "checkVerification",
"module": "core",
"action": "condition",
"options": {
"if": "{{verifyTurnstile.success}}",
"then": {
"steps": [
{
"name": "processForm",
"module": "dbupdater",
"action": "insert",
"options": {
"sql": {
"table": "submissions",
"values": [
{
"column": "email",
"value": "{{$_POST.email}}"
}
]
}
}
}
]
},
"else": {
"steps": [
{
"name": "returnError",
"module": "core",
"action": "response",
"options": {
"status": 403,
"data": {
"error": "Verification failed",
"error_codes": "{{verifyTurnstile.error_codes}}"
}
}
}
]
}
}
}
]
}
}Response Data
The verification action returns the following data:
| Property | Type | Description |
|----------|------|-------------|
| success | boolean | true if verification passed, false otherwise |
| error_codes | array | Array of error codes if verification failed |
| challenge_ts | string | ISO timestamp of the challenge |
| hostname | string | Hostname where verification occurred |
| action | string | Action value set in the client widget |
| cdata | string | Custom data passed from the client |
Success Response Example
{
"success": true,
"error_codes": [],
"challenge_ts": "2025-10-04T12:00:00.000Z",
"hostname": "example.com",
"action": "login",
"cdata": null
}Error Response Example
{
"success": false,
"error_codes": ["invalid-input-response"],
"challenge_ts": null,
"hostname": null
}Common Error Codes
| Error Code | Description |
|------------|-------------|
| missing-input-secret | Secret key is missing |
| invalid-input-secret | Secret key is invalid |
| missing-input-response | Token is missing |
| invalid-input-response | Token is invalid or has expired |
| timeout-or-duplicate | Token has already been validated or has timed out |
| internal-error | Internal error occurred |
Complete Workflow Example
Client-Side (using wappler-cf-turnstile-client)
<form id="contactForm" dmx-on:submit.prevent="submitContact.load()">
<input type="email" name="email" placeholder="Email" required>
<input type="text" name="message" placeholder="Message" required>
<!-- Turnstile widget -->
<dmx-turnstile
id="turnstile1"
sitekey="<%= process.env.TURNSTILE_SITEKEY %>">
</dmx-turnstile>
<!-- Token automatically included as hidden input: cf-turnstile-response -->
<button type="submit" dmx-bind:disabled="!turnstile1.isVerified">
Submit
</button>
</form>
<dmx-serverconnect
id="submitContact"
url="/api/contact/submit"
dmx-param:email="contactForm.email.value"
dmx-param:message="contactForm.message.value"
dmx-param:cf-turnstile-response="turnstile1.token">
</dmx-serverconnect>Server-Side (/api/contact/submit.json)
{
"exec": {
"steps": [
{
"name": "verify",
"module": "turnstile",
"action": "verify",
"options": {
"secret": "{{$_ENV.TURNSTILE_SECRET_KEY}}",
"response": "{{$_POST['cf-turnstile-response']}}",
"remoteip": "{{$_SERVER.REMOTE_ADDR}}"
},
"output": true
},
{
"name": "condition",
"module": "core",
"action": "condition",
"options": {
"if": "{{verify.success}}",
"then": {
"steps": [
{
"name": "saveMessage",
"module": "dbupdater",
"action": "insert",
"options": {
"sql": {
"table": "messages",
"values": [
{"column": "email", "value": "{{$_POST.email}}"},
{"column": "message", "value": "{{$_POST.message}}"}
]
}
},
"output": true
}
]
},
"else": {
"steps": [
{
"name": "error",
"module": "core",
"action": "response",
"options": {
"status": 403,
"data": {
"error": "CAPTCHA verification failed"
}
}
}
]
}
}
}
]
}
}Security Best Practices
1. Use Environment Variables
Never hardcode secret keys:
{
"secret": "{{$_ENV.TURNSTILE_SECRET_KEY}}"
}2. Always Verify on Server
Never trust client-side verification alone. Always verify tokens on your server.
3. Include IP Address
Pass the user's IP for additional security:
{
"remoteip": "{{$_SERVER.REMOTE_ADDR}}"
}4. Check Success Status
Always check the success property before processing requests:
{
"if": "{{verify.success}}"
}5. Handle Errors Gracefully
Return appropriate error responses:
{
"status": 403,
"data": {
"error": "Verification failed",
"code": "{{verify.error_codes[0]}}"
}
}Testing
Test Keys
Cloudflare provides test keys for development:
Always passes:
- Site key:
1x00000000000000000000AA - Secret key:
1x0000000000000000000000000000000AA
Always fails:
- Site key:
2x00000000000000000000AB - Secret key:
2x0000000000000000000000000000000AB
Example Test
{
"name": "test",
"module": "turnstile",
"action": "verify",
"options": {
"secret": "1x0000000000000000000000000000000AA",
"response": "test-token"
},
"output": true
}Troubleshooting
Module Not Appearing
- Check extension is in
project.json - Verify files are in correct location
- Restart Wappler completely
"Invalid input secret" Error
- Check secret key is correct
- Verify environment variable is loaded
- Ensure no extra spaces in key
"Invalid input response" Error
- Token may have expired (tokens are valid for ~5 minutes)
- Token may have already been used
- Token may be from a different site key
Timeout Errors
- Check network connection
- Verify firewall isn't blocking Cloudflare API
- Default timeout is 10 seconds
Requirements
- Node.js >= 14.0.0
- Wappler with Server Connect
- Cloudflare Turnstile account
Related Packages
This extension requires the client-side companion:
- wappler-cf-turnstile-client - Client-side Turnstile widget component
- NPM: https://www.npmjs.com/package/wappler-cf-turnstile-client
- GitHub: https://github.com/evaldas/wappler-cf-turnstile-client
Install both extensions for complete functionality:
npm install wappler-cf-turnstile-client
npm install wappler-cf-turnstile-serverSupport
- Wappler Community: https://community.wappler.io/
- NPM: https://www.npmjs.com/package/wappler-cf-turnstile-server
- GitHub: https://github.com/evaldas/wappler-cf-turnstile-server
License
MIT - See LICENSE file
Author
Evaldas Sedys
Version: 1.0.0
Last Updated: October 2025
