Turnstile (optional login protection)
Every login form already has an always-on, per-IP rate limiter. On top of that, you can add a Cloudflare Turnstile human-check on the login screens. It's optional and off by default — skip this page entirely and login still works.
Turnstile protects the unauthenticated login requests (POST /api/auth/magic-link on each tenant, and POST /admin/dash/auth/login on the central dashboard). A signed-in member's request to change their email is not gated: the session already proves who is asking, and the request keeps its rate limits. It has two halves, both set through configuration — no code changes:
- Public sitekey — a
TURNSTILE_SITE_KEYvar on each worker ([env.<id>.vars]inapi/wrangler.toml,[env.legiscan.vars]incentral/wrangler.toml). The login form shows the widget only when this is set; unset means no widget. - Secret key — the
TURNSTILE_SECRET_KEYworker secret. The server checks the token only when this is set (and then fails closed: a missing or invalid token returns a 403). Unset means the check is skipped.
Setup
# 1. Create a Turnstile widget in the Cloudflare dashboard. On the widget, list your
# registrable domain (one hostname covers all its subdomains) plus `localhost`.
# Note the sitekey (public) and the secret key.
# 2. Add the sitekey var to each worker that should show the widget, e.g.
# api/wrangler.toml: [env.org-nj.vars] TURNSTILE_SITE_KEY = "0x...."
# central/wrangler.toml: [env.legiscan.vars] TURNSTILE_SITE_KEY = "0x...."
# 3. Set the secret on each worker (tenants from api/, dashboard from central/):
wrangler secret put TURNSTILE_SECRET_KEY --env org-nj
# dashboard
wrangler secret put TURNSTILE_SECRET_KEY --env legiscanWARNING
Order matters. Because the check fails closed once the secret is set, setting the secret before the login form is sending a token will 403 every login. Deploy the sitekey-bearing frontend first, then set the secret.
WARNING
A sitekey without the secret looks like it works. The widget appears and visitors complete it, but the server skips the check, so the login form is protected by the rate limiter alone. When you add a tenant, set both. scripts/new-instance.sh does this for you, and refuses to continue if the sitekey is set but it can't get the secret. To check an existing tenant, a login request without a token should come back 403 Verification required.:
curl -s -X POST -H 'Content-Type: application/json' -d '{"email":"check@example.invalid"}' https://<tenant-host>/api/auth/magic-linkTo turn Turnstile off later, delete the secret — the check reverts to "skipped" instantly, no redeploy needed:
wrangler secret delete TURNSTILE_SECRET_KEY --env org-njThe full maintainer reference lives in docs/internal/turnstile.md in the repository.