Skip to main content

Knowledge > Integrations > Bot Protection

CURRENT STRATEGY (2026-04-06): Primary: Vercel's built-in Bot Protection (Vercel Firewall) — active on all projects, configured in Vercel dashboard, no code changes needed. Backup/Supplemental: Cloudflare Turnstile — retained as a planned additional layer for forms. Not yet implemented in code (turnstile.ts, Turnstile.tsx, verify-captcha/route.ts do not exist yet). Decision: Founder confirmed April 2026 — use Vercel Bot Protection as primary, keep Cloudflare Turnstile as backup. Implement Turnstile on onboarding form if bot signups become an issue.

Bot Protection Strategy

Primary: Vercel Bot Protection

Vercel's built-in bot protection is active at the infrastructure level — no code changes required. It blocks known bot IPs, crawlers, and scraper traffic before requests reach the Next.js application.

Configure at: Vercel Dashboard → Project → Settings → Security → Bot Protection

All three projects (churchwiseai-web, pewsearch, sermon-illustrations) should have this enabled.


Backup: Cloudflare Turnstile (Planned)

Cloudflare Turnstile is the planned CAPTCHA replacement for public-facing forms in churchwiseai-web. It is invisible or minimally-intrusive (compact widget), privacy-preserving, and free. Not yet implemented — keep for cases where form-level bot protection is needed beyond what Vercel provides.


Account

FieldValue
ProviderCloudflare
Dashboardcloudflare.com → Turnstile
Accountjohn@churchwiseai.com
Codebasechurchwiseai-web only

How It Works

  1. A form renders the <Turnstile /> component (client-side widget)
  2. Cloudflare analyzes the browser/user and issues a one-time token
  3. On form submit, the token is sent to the API route alongside the form data
  4. The API route calls verifyTurnstile(token) which POSTs to Cloudflare's verification endpoint
  5. If verification passes, form processing continues; otherwise it returns 403

Key Files

src/lib/turnstile.ts — Server-side verification

export async function verifyTurnstile(token: string): Promise<boolean>
  • Calls https://challenges.cloudflare.com/turnstile/v0/siteverify
  • Fails closed in production — if TURNSTILE_SECRET_KEY is not set, returns false
  • Bypasses in dev — if secret key is missing in non-production, logs a warning and returns true (so forms work without Cloudflare config locally)

src/components/Turnstile.tsx — Client widget

import { Turnstile } from '@/components/Turnstile';
// ...
<Turnstile onSuccess={(token) => setTurnstileToken(token)} />
  • Wraps @marsidev/react-turnstile
  • size: 'compact', theme: 'light', refreshExpired: 'auto'
  • Returns null silently if NEXT_PUBLIC_TURNSTILE_SITE_KEY is not set (forms still work without widget in dev)

src/app/api/auth/verify-captcha/route.ts — Standalone verification endpoint

POST /api/auth/verify-captcha — accepts { turnstileToken }, returns { ok: true } or 403. Rate-limited to 10 requests/min per IP. Used by forms that verify captcha as a separate pre-flight step.


Forms That Use Turnstile

FormFileHow it verifies
Church onboardingsrc/app/onboard/components/OnboardForm.tsxToken sent with form POST
Contact formsrc/app/contact/components/ContactForm.tsxToken sent with form POST
Newsletter signupsrc/components/FooterNewsletter.tsxToken sent to /api/newsletter
Care subscribesrc/components/care/CareSubscribeForm.tsxToken sent to /api/care/subscribe
Auth signupsrc/components/auth/SignupForm.tsxToken sent with signup request

Environment Variables

VariableWherePurpose
NEXT_PUBLIC_TURNSTILE_SITE_KEYVercel (production + preview)Public key — embedded in the client-side widget
TURNSTILE_SECRET_KEYVercel (production + preview)Server secret — used to verify tokens with Cloudflare

Both keys come from the Cloudflare Turnstile dashboard. Each domain/subdomain can have its own site key, or you can use a wildcard site key.

Dev note: Neither key is needed for local development. The widget returns null and verifyTurnstile() bypasses with a warning log.


Behavior Summary

EnvironmentWidget shownVerification
Production (keys set)Yes (compact, light)Real Cloudflare check
Dev (no keys)Hidden (returns null)Always passes with console warn
Production (keys missing)HiddenAlways fails (fail-closed)

See Also