feat(contact): add Cloudflare Turnstile to the contact form - #30
Merged
Conversation
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.
Deploying with
|
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.