| Duration | ~45 min in the lesson + ~20 min homework |
| Prerequisites | Checkpoint lesson-4.3; a Cloudflare account. |
| Checkpoint | lesson-4.4 |
What you will have
Your lead endpoint is protected by three layers — a honeypot field, a rate limit and Cloudflare Turnstile verified on the server. A bot script is rejected, and a normal visitor still submits the form without noticing anything.
Video
The video for this lesson is not recorded yet.
Prompts used in this lesson
Part 5 — Build the honeypot and Turnstile
Purpose: stop bots from filling the leads table, without making the form harder for real visitors. Context: checkpoint lesson-4.3. /api/leads validates and saves leads. I created a Cloudflare Turnstile widget and set NEXT_PUBLIC_TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY myself: Cloudflare's test keys in .env.local, real keys in Vercel Production. Do not open or print .env.local. Rate limiting is handled separately by a Vercel Firewall rule; do not implement rate limiting in code. Done looks like: 1. Honeypot: the form has one extra field that sighted users, keyboard users and screen-reader users never meet, and that browsers do not autofill. If it arrives non-empty, the endpoint responds exactly like a success but saves nothing and logs the word "honeypot" without personal data. 2. Turnstile in the form: the widget loads on the lead form in both places it appears, the token is sent with the submission, and after any failed submission the widget is reset so the visitor gets a fresh token (tokens are single-use). 3. Turnstile on the server: the endpoint first validates the fields, then verifies the token with Cloudflare's Siteverify API using the secret key, and saves the lead only if verification succeeds. Missing token, failed verification, or Cloudflare being unreachable all mean the lead is not saved and the response is 403 with a friendly "could not verify, please try again" message. 4. The form shows that message and stays usable. 5. docs/architecture.md describes the order of checks. /privacy mentions that the form uses Cloudflare Turnstile to prevent spam. Constraints: follow Cloudflare's current Turnstile documentation for the client rendering and the Siteverify request; read it rather than relying on memory. The secret key is used only on the server. Keep the accessibility results from lesson 3.6: run the automated accessibility check again. Before you report: with the dev server running, show me the status and response for (a) a normal browser-style submission flow, (b) a POST with the honeypot filled, (c) a POST with no token, and confirm in each case whether a row was added. Run the production build.
Part 7 — The bot test
Purpose: prove that the three spam defences work on the live site by imitating a bot. Done looks like: a script run as `npm run test:bot -- <base-url>` that sends POST requests to <base-url>/api/leads, all with the name "TEST bot" and otherwise valid field values: A. honeypot field filled, no token; B. honeypot empty, no Turnstile token; C. honeypot empty, a made-up token string; D. a burst of 15 copies of request B as fast as possible. It prints the status code of each request and a summary: A expected "looks like success" (and I will confirm no row was saved), B and C expected 403, D expected to switch to 429 part-way through. Final line PASS or FAIL. Also update `npm run test:endpoint` from lesson 4.3: a direct request without a token can no longer succeed, so its last case now expects 403. Constraints: the script uses no keys and reads no env files. Do not run attack C against the local server: with Cloudflare's "always passes" test secret, any token is accepted there by design. Before you report: do not run the script yourself against the live site; I will run it.
Do along
Pause the video where a step says so and do it on your own project.
- Pause after Part 4. Create a Turnstile widget for your own domain in Managed mode.
- Put the test keys in
.env.local; real keys in Vercel Production, test keys in Preview. - Pause after Part 5. Give Claude the prompt from Part 5; submit a normal lead locally.
- Pause after Part 6. Create and publish the Firewall rate-limit rule with a limit that suits your business.
- Commit, push, wait for the deployment.
- Pause at the end of Part 7. Give Claude the prompt from Part 7 and run
npm run test:botagainst your live domain. - Wait a minute, then submit a normal lead on the live site.
Check your work
- Run
npm run test:bot -- https://<your-domain>. Expected: A looks like success, B and C return 403, the burst ends in 429, final line PASS. - Search
leadsfor "TEST bot" in the Supabase table view. Expected: no rows. - Wait one minute. Submit "TEST human" through the live form. Expected: thank-you page, and the row appears.
- Use the live form with the keyboard only. Expected: you never land on the honeypot field.
Common problems
- The widget shows an error on the live site → the domain is not in the widget's hostname list, or the sitekey in Vercel is the test key → fix the hostname in the widget's Settings in Cloudflare, check the Production variable, redeploy.
- Every real submission on the live site returns 403 → real sitekey paired with the test secret, or the reverse → both Production variables must come from the same widget; redeploy after changing them.
- The burst never returns 429 → the rule was saved but not published, or the path does not match → open Firewall, publish pending changes, and check the path is exactly
/api/leads. - A password manager fills the hidden field and real leads vanish → the honeypot is being autofilled → tell Claude: "the honeypot was autofilled by the browser; make it ignored by autofill and test again".
Homework
About 20 minutes. Keep written answers in your own notes, outside the project folder. Do not change the rate-limit rule or the keys.
- A real visitor on another device. On your phone, or in a browser you do not normally use, submit a lead named "TEST Visitor" on your live domain. Watch what the Turnstile widget shows. Done when: the row is in the table and your notes say what the visitor saw and whether anything slowed them down.
- Read what your site now says. Open
/privacyon the live site and find the sentence about Cloudflare Turnstile. Check that it is true for your site and reads like the rest of the page. This is not legal advice. Done when: the sentence is fine as it is, or you had Claude reword it and committed without a tag. - Your "junk got through" plan. Write four lines: where you would notice junk leads, how you would tell which layer missed them (honeypot, rate limit, Turnstile), which test script you would rerun, and what you would tell Claude. Add the rate limit you chose and one sentence on why it fits your customers. Done when: the plan fits on half a page and names both scripts,
test:botandtest:endpoint.
Save your work
git add -A
git commit -m "Lesson 4.4: honeypot, rate limit and Turnstile"
git tag lesson-4.4
git push
git push --tags