Skip to content

feat(contact): add Cloudflare Turnstile to the contact form - #30

Merged
dyascj merged 1 commit into
mainfrom
feat/turnstile-contact-form
Jul 31, 2026
Merged

feat(contact): add Cloudflare Turnstile to the contact form#30
dyascj merged 1 commit into
mainfrom
feat/turnstile-contact-form

Conversation

@dyascj

@dyascj dyascj commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Turnstile managed mode, free at this scale: 20 widgets per account and a million siteverify calls a month. The widget is already created on the account and the secret is already set on the Worker.

Please check this yourself before merging

I could not verify the path that matters. Turnstile refuses to issue a token to an automated browser, which is precisely its job, so headless Chromium sat there for twenty seconds and got nothing. What I proved is that the widget renders with the right site key and that a submission carrying no token is rejected. What I could not prove is that a real person can fill the form in and send it.

Open https://cjdyas.design/contact in a normal browser after this merges and send yourself a message. If it goes through, it works.

Worth knowing before you decide

A missing token is rejected. If someone runs a blocker that eats challenges.cloudflare.com, their submission fails with "Could not verify you are human" and they have no way around it. That is the standard tradeoff for a CAPTCHA, but it is a real cost on a contact form that exists to let strangers reach you.

If siteverify itself is unreachable the submission is allowed through instead, so a Cloudflare outage cannot silently kill the form. The rate limiter sits in front either way.

Implementation notes

The site key is a wrangler var, not $env/static/public. A static import is baked in at build time and the CI builder has no .env.local, so it would have shipped empty and the widget would never have rendered.

The e2e stub that used to cover Vercel Analytics now covers Turnstile, since it logs its own noise to console.error and would otherwise fail the strict no-console-errors assertion.

Verification

18 unit tests including five new ones covering approval, rejection with reason, empty token short-circuit, unconfigured skip, and the unreachable-endpoint fallback. 28 e2e against workerd. Typecheck, lint and build clean.

Turnstile's managed mode is free with no practical ceiling here: 20 widgets
per account and a million siteverify calls a month.

The widget only renders when a site key is present, and verification is
skipped entirely when no secret is configured, so plain vite dev and the
test suite are unaffected. The site key comes from a wrangler var rather
than $env/static/public, because a static import is baked in at build time
and the CI builder has no .env.local; it would have shipped empty.

Two deliberate escape hatches. A missing token is rejected, but if
siteverify itself is unreachable the submission is allowed through rather
than letting a Cloudflare outage silently kill the contact form. The rate
limiter still sits in front of it either way.

The e2e stub that existed for Vercel Analytics now covers Turnstile
instead, since Turnstile logs its own noise to console.error and will not
issue a token to an automated browser, which is rather the point of it.

Verified against workerd: the widget renders with the site key, a
submission with no token is rejected, 18 unit tests, 28 e2e, typecheck and
build clean. The human path is unverified by design, see the PR.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
cjdyas-design 7dce7b0 Commit Preview URL

Branch Preview URL
Jul 31 2026, 07:09 PM

@dyascj
dyascj merged commit f5fb05f into main Jul 31, 2026
3 checks passed
@dyascj
dyascj deleted the feat/turnstile-contact-form branch July 31, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant