usarise/turnstile
A Laravel package for integrating Cloudflare Turnstile into your forms, with easy setup, server-side validation, configurable keys, and Blade helpers to protect your app from bots and spam.
Architecture fit. usarise/turnstile is a framework-agnostic PHP client for Cloudflare's Turnstile siteverify endpoint, not a Laravel package. It has no service provider, config publish, validation rule, or Blade component. That means integration is "install the SDK + write the Laravel glue yourself" — the glue being a config entry, a Turnstile wrapper service, a ValidationRule, and a hidden input/JS snippet. This is architecturally neutral, which is fine, but it means adopting this package buys us roughly 50–80 lines of code we would otherwise write directly against Http::post('https://challenges.cloudflare.com/turnstile/v0/siteverify'). The build-vs-buy math is dominated by the fact that the underlying API is a single POST with a 4-field request and a small error-code vocabulary.
Integration feasibility. High on effort terms: composer require usarise/turnstile, bind a client in a service provider, add TURNSTILE_SITE_KEY / TURNSTILE_SECRET to env, register a validation rule, drop the <script src="https://challenges.cloudflare.com/turnstile/v0/api.js"> tag in the layout. A mid-level Laravel engineer completes a working vertical slice in under a day. The friction is not technical, it is governance: the repo has 23 stars, 0 dependents, and an Unlicense. Zero dependents is a stronger signal than the star count — it means no observable production usage in the ecosystem to learn from.
Technical risk.
Http factory or does its own cURL. If it does its own cURL, we lose Http::fake(), lose Laravel's retry/timeout middleware, and lose observability hooks — that is the single biggest practical objection.siteverify returns timeout-or-duplicate for replays. The package cannot protect us from misusing tokens across retries, double-submits, or SPA route changes; that logic is ours.Key questions for the maintainer / spike.
error-codes array, or collapse to a boolean? We need timeout-or-duplicate distinct from invalid-input-response for alerting.remoteip supported and is it sent by default (privacy review depends on this)?Stack fit. Works on any Laravel 9+ / PHP 8.1+ application with Guzzle present. It is transport-agnostic in principle, so it fits a standard Laravel app, an Octane worker, and a queued job equally — no global state, no session coupling. It does not fit our admin SPA forms without extra work, since Turnstile widgets must be re-rendered/reset per submission.
Competitive context. There are established Laravel-native Turnstile packages with validation rules, Blade components, and config publishing built in. Choosing this one means explicitly choosing "lean dependency + own glue" over "batteries included + larger community." I would make that a deliberate decision, not a default: pick this package only if the glue is genuinely trivial for us and we want the smallest possible third-party surface, otherwise the Laravel-native option reduces our maintenance load for the same API call.
Migration path. Assume we are moving off reCAPTCHA v2/v3 or hCaptcha.
cf-turnstile-response; scripts come from challenges.cloudflare.com — both need CSP allowlisting (script-src and frame-src), which is often the slowest step in a locked-down app.captcha.driver = recaptcha|turnstile) so verification is swappable per environment and per route.Compatibility. Verify the package's PHP constraint against our fleet before promising a date; if we run mixed 8.1/8.3 services, pin per-service. Also confirm it does not pull a Guzzle major that conflicts with our existing lockfile — a dependency-resolution failure here is the most likely cause of an unexpected multi-day slip.
Sequencing (est. 3–4 weeks elapsed, ~5–7 engineer-days).
Maintenance. Budget ~1–2 days per year for dependency bumps, plus the real cost: the wrapper is ours, so schema changes to siteverify are our incident, not an upstream patch we can wait for. The zero-dependent status means we will not benefit from community bug reports — we are the canary. Recommendation: isolate all package calls behind a single TurnstileVerifier interface with a 40-line fallback implementation committed in-repo, so a swap or a fork is a one-file change rather than a refactor. Because the surface is one POST, this abstraction costs almost nothing and converts an unbounded supply-chain risk into a bounded one.
Support. No SLA, no vendor, GitHub issues only. Escalation path is our own engineer or a Cloudflare status page. Set expectations with stakeholders explicitly: this is a "we own it" dependency, not a "someone supports it" dependency.
Scaling. Effectively free — one outbound HTTPS call per protected form submission, negligible compared to the request itself. Two caveats: (a) every bot that reaches our form still costs us an outbound call plus a Cloudflare call, so validation must sit behind existing rate limiting, not replace it; (b) Cloudflare applies rate limits to siteverify per sitekey and returns HTTP 429 — we need a 429-aware branch, not a generic failure, or a bot flood degrades into a site-wide form outage. Token validity is ~300s, so long-lived forms (multi-step checkout) must reset the widget before submit.
Failure modes to design for, in priority order.
siteverify becomes slow page submits and thread/worker pile-up under Octane.timeout-or-duplicate returned on retries; users double-clicking see a spurious "captcha failed." Needs idempotent submit handling and a widget reset.invalid-input-response rate, not just on 5xx.Ramp-up. Low. Any Laravel developer can own this with the README plus a one-page internal runbook. The real onboarding cost is the fail-open/fail-closed policy and the alerting thresholds, which are product decisions, not engineering ones — assign an owner for those before the first form goes live.
How can I help you explore Laravel packages today?