Answers · Keys & Services

How do I stop bot signups on a Rails sign-up form?

Set TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY. app/adapters/bot_check.rb returns a real Cloudflare Turnstile verifier when both are present and a pass-everything fake when they are not, so the form works in development with no account and starts blocking bots in production without a code change.

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.

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.

Related questions

Do I need API keys to run a Rails starter kit locally?

No. Billing, email, bot checking and error reporting each sit behind an adapter that returns a real client when its credential is present and a deterministic local fake when it is not. A fresh clone runs bin/setup and bin/dev with no keys, and checkout, sign-in and gated features all work end to end.

How do I store uploaded files on S3 instead of local disk?

Set S3_BUCKET, S3_REGION, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, plus S3_ENDPOINT for a non-AWS provider. config/storage.yml already defines the service, so production uses object storage instead of the container's disk. Existing files on local disk do not move by themselves.

How do I tell which integrations are live in production?

Ask the adapters at runtime rather than reading environment variables. Each one chooses a real client or a local fake based on whether its credential is present, so querying the adapter tells you what the app is actually doing. A variable that is set but misspelled looks configured and behaves like a fake.