#TurnstileCaptchaProvider
#Import
1import { TurnstileCaptchaProvider } from "alepha/captcha";
#Overview
Cloudflare Turnstile captcha verification provider.
Validates captcha tokens against the Cloudflare Turnstile siteverify API. Free, privacy-friendly, and supports invisible mode.
#Setup
- Create a Turnstile widget at https://dash.cloudflare.com/?to=/:account/turnstile
- Copy the Site Key (public, for the client) and Secret Key (private, for the server)
- Set
TURNSTILE_SECRET_KEYandTURNSTILE_SITE_KEYin your environment (both required)
#Client-side integration
Add the Turnstile script and widget to your form:
1<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>2<form>3 <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>4 <button type="submit">Submit</button>5</form>
The widget injects a hidden cf-turnstile-response input into the form.
Send this value as the captchaToken in your registration request.
For explicit rendering (React, SPA):
1turnstile.render("#container", {2 sitekey: "YOUR_SITE_KEY",3 callback: (token) => setCaptchaToken(token),4});
#Server-side usage
Register the provider in your app:
1import { CaptchaProvider } from "alepha/captcha";2import { TurnstileCaptchaProvider } from "alepha/captcha";3 4alepha.with({ provide: CaptchaProvider, use: TurnstileCaptchaProvider });
#Test keys (for development)
- Always passes: site
1x00000000000000000000AA, secret1x0000000000000000000000000000000AA - Always blocks: site
2x00000000000000000000AB, secret2x0000000000000000000000000000000AB - Forces interactive: site
3x00000000000000000000FF
#Environment Variables
TURNSTILE_SECRET_KEY: The secret key from the Cloudflare Turnstile dashboard (required).TURNSTILE_SITE_KEY: The public site key, exposed to the client viagetSiteKey()(required).TURNSTILE_EXPECTED_HOSTNAME: Refuse a token solved on any other host (optional).TURNSTILE_EXPECTED_ACTION: Refuse a token solved for any other widget action (optional).
#Pinning the hostname and the action
A token is bound to the site it was solved on and the action its widget
declared, and siteverify reports both back. Nothing checks them unless you
say what to expect, so by default a token farmed from one of your pages is
accepted on any other - the login widget's token works against the
registration endpoint, and a token solved on a site sharing your secret
works anywhere.
Both are opt-in because both are easy to get wrong: an app served from an
apex and a www host, or from preview deployments, has more than one valid
hostname, and a single mismatch refuses every registration.