Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Turnstile Laravel Package

muazzambuilds/filament-turnstile

Easily add Cloudflare Turnstile CAPTCHA protection to Filament forms with this lightweight package. It blocks spam and bot submissions using a privacy-friendly challenge, offers simple configuration, and keeps the experience smooth for real users. Quick to install and configure in minutes.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm constraints first. The release notes give you the install line and nothing else, so treat composer.json as the real documentation:

composer require muazzambuilds/filament-turnstile

Before wiring anything up, check two things in the installed package:

  1. The Filament constraint. This is a Filament v5 plugin. If your project is on v3/v4, Composer will either refuse to install or you'll be reading source for an API you don't have. Confirm filament/filament: ^5.0 in the package's require block matches your app.
  2. What it ships. Run php artisan vendor:publish --provider="...TurnstileServiceProvider" (or --tag=turnstile-config) and inspect the config file. That file, plus the src/ tree, tells you the env var names and the component class name — the two things you need and the two things the release notes don't provide.

Credentials. Go to the Cloudflare dashboard → Turnstile → Add Widget. You get a site key (public, rendered into the page) and a secret key (server-side only). Put them in .env using whatever keys the config file expects — conventionally TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY. Add both to .env.example with empty values and to your deploy secrets.

Register the plugin. In the relevant panel provider:

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(FilamentTurnstilePlugin::make()) // confirm class name from src/
        ...
}

First use case: the admin login form. This is where the package earns its keep on day one. Filament's login is the most exposed form on most installs and the usual target for credential stuffing. A Turnstile challenge there is a five-minute win — register the plugin, add the credentials, log out, and confirm the widget renders above the sign-in button. If it renders and a real login succeeds, your keys, domain allowlist, and plugin registration are all correct in one pass.

Look at README.md → src/ → config/ in that order. For a v1.0.0 package the README is often thinner than the code, and the config file is usually the most honest summary of what's configurable.

Implementation Patterns

Protecting the panel login. The plugin's primary affordance is injecting the widget into Filament's auth forms. Once registered, verify the flow end to end: the widget must render and a submission with a valid token must pass server-side verification. A widget that renders but is never verified is worse than nothing — it looks like protection.

Adding the field to your own forms. For resource forms, custom pages, or registration flows, you'll use the package's form component alongside your existing schema:

use MuazzamBuilds\FilamentTurnstile\Forms\Components\Turnstile; // confirm namespace

->schema([
    TextInput::make('name')->required(),
    // ...
    Turnstile::make('cf_turnstile'),
])

Confirm the exact class and method names against src/ — a first release has no community-settled conventions to lean on. If the package doesn't expose a standalone field, fall back to rendering the Turnstile script yourself and calling siteverify from a ->action() or a form ->using() closure.

Scope it to the panels that need it. On multi-panel apps (admin, staff, customer), you usually want the challenge on customer-facing and public-facing auth only. Registering the plugin per-panel provider — rather than in a shared base — keeps internal panels frictionless for staff who are already behind SSO or a VPN.

Gate it by environment. Most teams run the challenge everywhere except local and CI:

->plugin(FilamentTurnstilePlugin::make()->enabled(app()->isProduction()))

Check whether the package exposes an enabled()/disabled() toggle; if not, conditionally register based on app()->environment(). Cloudflare's published test keys (which always pass and always fail respectively) are the better option for staging — they exercise the full verification path without the domain-allowlist headaches.

Server-side verification is the contract. Turnstile's widget is cosmetic on its own; the security guarantee comes entirely from your server posting the token to https://challenges.cloudflare.com/turnstile/v0/siteverify with the secret and acting on success: true. Whatever the package does, be able to answer: where in this request lifecycle does that call happen, and what happens if it returns false? If you can't point at the code, assume it doesn't and add your own check in a form ->using() closure or a middleware.

Testability. Write one test asserting that a submission without a token is rejected. Then stub the verification call (Http::fake() on the siteverify URL) so the rest of your suite doesn't hit Cloudflare. Keep that rejection test even if you bypass Turnstile in the test environment — it's the regression guard for someone accidentally removing the check.

Gotchas and Tips

Domain allowlist is the #1 first-day failure. A Turnstile widget is bound to the hostnames configured on it. localhost, 127.0.0.1, preview deploy URLs, and staging subdomains all need to be added explicitly (or use the test keys). The symptom is a widget that renders but immediately errors — easy to misdiagnose as a package bug.

Tokens are single-use and expire in ~5 minutes. Two consequences: (a) you cannot reuse a token across forms or across a retry, and (b) after a failed validation the token is spent. In Livewire/Filament, a re-render can leave a stale, already-consumed token in the DOM. If users report "Turnstile keeps failing", the fix is almost always resetting the widget (turnstile.reset()) after each submit attempt. Check whether the package handles this; if not, hook it into the form's submit lifecycle.

Livewire remounting. Filament forms re-render aggressively. A widget mounted in a component that gets torn down and rebuilt can end up with an invisible or duplicated iframe. If the challenge doesn't appear after a modal opens or a wizard step advances, this is why — look for a wire:ignore wrapper around the widget container.

Secret key leakage. The site key is public by design; the secret key is a bearer credential. It must never appear in a Blade view, a Livewire component's public properties (they serialize to the browser), or a VITE_-prefixed env var. If you see the secret in the rendered HTML, stop and fix it before deploying.

Content Security Policy. If you run a CSP, Turnstile needs https://challenges.cloudflare.com allowed in script-src, frame-src, and connect-src. A silent failure with a strict CSP is normal; check the browser console before blaming the package.

Turnstile is not rate limiting. It raises the cost of automated attacks but doesn't replace throttling. Keep Filament's login rate limiter, and keep whatever you have on registration and password reset. Turnstile also does not protect against a determined human-driven attacker or a compromised credential — pair it with 2FA on admin panels.

Pin the version. v1.0.0 is an initial release: the API surface has no deprecation history behind it, so a ^1.0 constraint is reasonable but expect breaking changes in 1.x minors. Pin to an exact version if this is going into a client project, and read the changelog on each bump rather than trusting semver.

Extension points to look for when you need to go beyond defaults: a config-driven appearance/theme/size/language setting (Turnstile supports light/dark/auto and explicit hl locales — worth matching to your panel's locale), a publishable view you can override for layout tweaks, and an enabled toggle or environment check so you don't have to comment out the plugin registration. If none of those exist, the pragmatic route is a thin custom component wrapping the official Turnstile script tag, with the package left in place for the auth-forms case it already solves.

Debugging order when it doesn't work: (1) does the widget render? (2) browser console for CSP/domain errors, (3) Network tab — is siteverify being called at all, and what does the JSON say (error-codes is explicit: invalid-input-secret, timeout-or-duplicate, invalid-input-response), (4) is the secret key present in the running process's env (config caching — php artisan config:clear). The timeout-or-duplicate code and the "widget renders but form submits anyway" symptom both point at the single-use token issue above.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator