Turnstile CAPTCHA
Turnstile is Cloudflare’s CAPTCHA alternative. A Turnstile widget is
defined by the hostnames it runs on and its interaction mode;
Cloudflare assigns two keys in return — a public sitekey you embed
in HTML and a secret your server uses to verify the tokens the
widget produces.
Create a widget
Section titled “Create a widget”import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "SignupApp", { providers: Cloudflare.providers(), state: Cloudflare.state(), }, Effect.gen(function* () { const widget = yield* Cloudflare.Turnstile.Widget("SignupForm", { domains: ["example.com"], mode: "managed", });
return { sitekey: widget.sitekey, }; }),);domains lists the hostnames the widget may run on — subdomains are
covered automatically. mode is managed (Cloudflare decides
whether an interaction is required), non-interactive (never asks),
or invisible (no visible widget). If you omit name, a unique one
is generated from the app, stage, and logical ID.
The two keys
Section titled “The two keys”const sitekey = widget.sitekey; // public — render it in your HTMLconst secret = widget.secret; // Redacted<string> — server-side onlyThe sitekey is public and safe to expose as a stack output. The
secret is a Redacted<string>: it never prints in logs or
JSON-serialized output (it renders as "<redacted>"), so unwrap it
with Redacted.value at the point where your server actually needs
the raw string.
Embed the sitekey
Section titled “Embed the sitekey”<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script><div class="cf-turnstile" data-sitekey="<sitekey from the deploy output>"></div>The widget runs the challenge in the browser and puts the resulting
token in a hidden cf-turnstile-response form field (or hands it to
your callback). The token proves nothing by itself — your server must
verify it.
Verify tokens server-side
Section titled “Verify tokens server-side”There is no runtime binding for Turnstile — verification is a plain
HTTPS call from any backend. POST the token together with
Redacted.value(secret) to Cloudflare’s
https://challenges.cloudflare.com/turnstile/v0/siteverify endpoint
and check success in the response; see Cloudflare’s
server-side validation docs
for the full request/response contract. A Worker is a natural place
to do this — pass the secret in via the Worker’s env.
Updates and replacement
Section titled “Updates and replacement”const widget = yield* Cloudflare.Turnstile.Widget("SignupForm", { domains: ["example.com", "app.example.com"], mode: "invisible",});Name, domains, mode, and the clearance settings all mutate in place —
the widget keeps its sitekey and secret, so deployed pages keep
working. Only region (default "world") forces a replacement,
which mints a new sitekey and secret. If the widget is deleted
out-of-band, the next deploy observes it missing and recreates it
rather than failing.
Where next
Section titled “Where next”- Workers — host your siteverify call next to the form it protects.
- Secrets & env — how the widget secret gets into the Worker.
- Widget API reference