> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vibestrap.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Turnstile (anti-bot)

> Cloudflare Turnstile gate on signup, forgot-password, and newsletter forms — privacy-friendly, free, no cookies.

Public forms (signup, forgot-password, newsletter) get scraped by bots within hours
of going live — fake signups poison your audience, password-reset spam burns through
your email quota, and your Stripe risk score takes a hit. vibestrap protects all
three with [Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/),
a CAPTCHA replacement that's privacy-friendly, free at any volume, and doesn't drop
any cookies.

The widget is wired into the signup, forgot-password, and newsletter forms; the
server-side helper at `src/lib/turnstile.ts` verifies the token before the action
runs. If env vars are unset, verification gracefully returns `{ ok: true,
skipped: true }` so dev and unconfigured deployments keep working.

## Why Turnstile (vs reCAPTCHA)

* **Privacy-friendly.** No cookies, no Google tracking, GDPR-friendly out of the box.
* **Free at any scale** — no usage cap.
* **Lower user friction** — most challenges are invisible (managed mode).
* **Cloudflare ecosystem** — if you already proxy through Cloudflare, this is
  the obvious choice.

## Prerequisites

* A Cloudflare account (free tier is fine).
* Your domain registered as a Turnstile site at
  [dash.cloudflare.com](https://dash.cloudflare.com/) → Turnstile → Add Site.
* The site key and the secret key from that site's settings page.

## Step-by-step

1. **Create a Turnstile site** in the Cloudflare dashboard. Add your
   production domain plus `localhost` for local dev. Pick "Managed" mode
   for the best UX (Cloudflare decides when to challenge).

2. **Set both env vars** in `.env.local`:

   ```bash theme={null}
   NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAAA...     # public, ships to client
   TURNSTILE_SECRET_KEY=0x4AAAAAAA...                # server-only, never exposed
   ```

3. **Confirm the feature flag** in `src/config/site.ts` (already on by default):

   ```ts theme={null}
   features: {
     enableTurnstile: true,
   },
   ```

4. **Restart `pnpm dev`.** The widget renders on the signup, forgot-password,
   and newsletter forms. The server actions automatically call
   `verifyTurnstile(token)` and reject the request when the token is missing
   or invalid.

## Verify it works

1. Open `/register` in incognito — you should see the Turnstile widget
   (usually invisible, or a quick "verifying" flash).
2. Open devtools → Network. On submit, the form posts a `cf-turnstile-response`
   field. The server action calls `https://challenges.cloudflare.com/turnstile/v0/siteverify`
   internally; you'll see no extra outbound request from the browser.
3. Try submitting with `NEXT_PUBLIC_TURNSTILE_SITE_KEY` removed — the widget
   disappears. Server-side, `turnstileEnabled()` returns `false` and verification
   is skipped (returns `{ ok: true, skipped: true }`).
4. Tamper with the token via devtools → server action returns an error,
   form does not submit.

## Common pitfalls

1. **Token can only be used once.** If a server action retries internally with
   the same token (e.g. after a transient DB error), the second call will fail.
   Either reset the widget on retry or short-circuit at the action boundary.
2. **Token expires after \~5 minutes.** Long-lived form pages (e.g. a multi-step
   signup) need to refresh the widget before submit. Turnstile's JS will
   auto-refresh if you call `turnstile.render` with `expired-callback`.
3. **Don't trust the client widget alone.** A green widget without server
   verification is theatre. `verifyTurnstile()` is what actually protects you;
   never bypass it in your own server actions.
4. **Missing token in production.** Common cause: `TURNSTILE_SECRET_KEY` set,
   but `NEXT_PUBLIC_TURNSTILE_SITE_KEY` not deployed (env var not added to
   the hosting platform). Result: the widget doesn't render → no token →
   server rejects every signup. Always deploy both vars together.
5. **Localhost not allow-listed.** Add `localhost` (and `127.0.0.1` if you use it)
   to your Turnstile site's domain list, or local signups will silently fail.

## How it's wired

```ts theme={null}
// src/lib/turnstile.ts (excerpt)
export async function verifyTurnstile(token: string | undefined | null) {
  if (!siteConfig.features.enableTurnstile || !env.TURNSTILE_SECRET_KEY) {
    return { ok: true, skipped: true };
  }
  if (!token) return { ok: false, skipped: false, errors: ['missing-token'] };
  // POST to Cloudflare siteverify, return parsed result.
}
```

Server actions call `verifyTurnstile(input.cfTurnstileToken)` first thing and
short-circuit on `!ok`. Adding it to a new form means: render the widget on
the client, pass the token through the action's input schema, and call
`verifyTurnstile` in the action body.

## Disable Turnstile for a deployment

Either:

* Set `siteConfig.features.enableTurnstile = false` (compile-time off), or
* Leave both env vars unset (runtime skip).

Either path makes `verifyTurnstile` return `{ ok: true, skipped: true }` so
the rest of the action proceeds normally.

## Official docs

* Turnstile docs: [developers.cloudflare.com/turnstile](https://developers.cloudflare.com/turnstile/)
* Server-side verification: [developers.cloudflare.com/turnstile/get-started/server-side-validation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/)
* Test keys: [developers.cloudflare.com/turnstile/troubleshooting/testing](https://developers.cloudflare.com/turnstile/troubleshooting/testing/)
