## Turn it on

Create a Turnstile widget in the Cloudflare dashboard, which is free, and copy the two keys. The site
key is public and renders in the page; the secret key verifies the token server side.

```bash
bin/kamal app exec 'bin/rails runner "puts BotCheck.enabled?"'
```

## Why it is an adapter rather than a gem call

Same reason as billing. `app/adapters/bot_check.rb` returns a verifier when the keys are set and a
fake when they are not, so:

- A fresh clone has a working sign-up form with no Cloudflare account.
- The test suite does not make network calls or depend on a third party being up.
- Turning it on is configuration, not a deploy of new code paths.

If you would rather use a different provider, the `add-integration` skill builds the same shape for
it, and the calling code does not change.

## Add Cloudflare's host to the policy

The widget loads a script from Cloudflare, so `script_src` in
`config/initializers/content_security_policy.rb` needs its host. The policy is enforced, so without
it the widget silently fails to render and the form submits without a token, which then fails
verification. Two silent failures stacked.

## What this does and does not stop

It stops automated scripted signups, which is the volume problem. It does not stop a determined
human, and it does not stop someone using a real email address to create throwaway accounts.

The other half of the defense is already there: Rack::Attack throttles by IP, configured in
`config/initializers/rack_attack.rb`. Note the public content paths are deliberately exempt so search
and AI crawlers are not rate limited, which is a different tradeoff from the sign-up form.

## Test the closed path

Verification failing has to actually block the signup. Set the secret key to a deliberately wrong
value locally and confirm the form rejects. An integration that fails open is worse than none,
because you believe you are protected.
