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

coderflex/filament-turnstile

Filament Turnstile adds Cloudflare Turnstile bot protection to Filament forms, with a ready-made field for login and other forms. Works with Filament V2-V5 and ships test keys so you can get started quickly.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Match the package major to your Filament major — before anything else. The version matrix is the single source of breakage:

Filament Package Notes
v5 ^4.0 Laravel 13, PHP 8.4
v4 ^3.0 Schema-based form API, laravel-turnstile ^2.0
v3 ^2.0 Forms\Form API
v2 ^1.0 legacy
composer require coderflex/filament-turnstile "^4.0"

2. Pull keys from Cloudflare and put them in .env. The package reads them through the underlying coderflex/laravel-turnstile config, so use the exact env names:

TURNSTILE_SITE_KEY=1x00000000000000000000AA
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA

Those are Cloudflare's dummy always-pass test keys — start with them so you know a failure is your code and not your keys. The matching always-fail pair is 2x00000000000000000000AB / 2x0000000000000000000000000000000AA.

3. Where to look first. The README here is thin by design; the real behaviour lives one layer down. Read, in order:

  • vendor/coderflex/laravel-turnstile/README.md — server-side verification, config keys, validation rule.
  • vendor/coderflex/filament-turnstile/src/Forms/Components/Turnstile.php — what the field actually registers (rule, dehydration, asset loading).
  • vendor/coderflex/filament-turnstile/resources/ — the Alpine/JS that listens for reset-captcha; this tells you whether the event is global or scoped per instance.

4. First use case: harden the panel login. This is 90% of real deployments. Subclass the page, add the field, override the panel's login class:

// app/Filament/Pages/Auth/Login.php
namespace App\Filament\Pages\Auth;

use Coderflex\FilamentTurnstile\Forms\Components\Turnstile;
use Filament\Pages\Auth\Login as AuthLogin;
use Filament\Schemas\Schema; // Filament v4/v5; use Filament\Forms\Form on v3

class Login extends AuthLogin
{
    public function form(Schema $form): Schema
    {
        return $form->schema([
            $this->getEmailFormComponent(),
            $this->getPasswordFormComponent(),
            $this->getRememberFormComponent(),
            Turnstile::make('captcha')->label('Captcha')->theme('auto'),
        ]);
    }
}
// AdminPanelProvider
->login(\App\Filament\Pages\Auth\Login::class)

Then submit the form once with a wrong secret key and confirm it fails. If it succeeds, your server-side verification is not wired up and you have a decorative widget — see Gotchas.

Implementation Patterns

Reset-on-failure is not optional. Turnstile tokens are single-use and expire (~5 min). Filament keeps form state across a failed submission, so the second attempt posts a spent token and gets timeout-or-duplicate — the user is permanently stuck. Wire the reset in both places:

// Generic Livewire form component
protected function onValidationError(ValidationException $exception): void
{
    $this->dispatch('reset-captcha');
}

// Login page specifically
protected function throwFailureValidationException(): never
{
    $this->dispatch('reset-captcha');
    parent::throwFailureValidationException();
}

Reset after success too, in flows where the same component stays mounted (repeat submissions, "add another" modals): dispatch reset-captcha after a successful save, alongside $this->form->fill().

Keep the token out of your model writes. The field stores the response token in form state. On any Resource create/edit form, add:

Turnstile::make('captcha')->dehydrated(false)

so $this->form->getState() / Model::create($data) never sees a captcha key. On auth pages the state is discarded anyway, so it's only mandatory on persisted forms.

Scope it by intent, not by fear. Don't captcha everything — it costs conversions and adds a third-party round trip:

Turnstile::make('captcha')
    ->visible(fn () => auth()->guest())
    ->visible(fn () => RateLimiter::tooManyAttempts('contact:'.request()->ip(), 3))

Registration and password-reset pages follow the identical pattern: subclass Filament\Pages\Auth\Register / RequestPasswordReset, override form(), and register via ->registration() / ->passwordReset() in the panel provider. Do all three or none — a captcha on login only is theatre.

Front-end (non-panel) forms. The component is a Filament field, so it renders inside any Livewire component using a Filament schema — but asset registration comes from the panel. If you use it outside a panel, expect to include https://challenges.cloudflare.com/turnstile/v0/api.js yourself and verify the widget mounts. In practice, keep Turnstile on panel/auth pages and use plain Cloudflare markup or laravel-turnstile directly on public marketing forms.

Testing. Never let CI talk to Cloudflare. Either flip turnstile.enabled to false in phpunit.xml, or fake the verify endpoint so you can also test the rejection path:

Http::fake([
    'challenges.cloudflare.com/*' => Http::response(['success' => true]),
]);

Then add one test with ['success' => false, 'error-codes' => ['invalid-input-response']] asserting the form does not submit. That test is your only real proof the rule is applied.

Gotchas and Tips

  • Silent no-op verification. The worst failure mode: the widget renders, looks fine, and the form submits with a garbage token. The rule comes from the field — don't hand-roll ->rules([...]), and don't assume it's active because the widget drew. Prove it with the dummy always-fail keys or a Http::fake returning success: false.
  • Hidden widgets get destroyed. Fixed in v2.4.0 ("destroyed when element visibility changes") and v2.4.1 ("multiple instances") — but if the captcha sits in a later wizard step, a collapsed section, a tab, or a modal, pin to ^2.4.1/^3.x/^4.x and sanity-check that the token arrives in state before submit. If it can't render, it can't be solved.
  • reset-captcha is a browser event by name. If two Turnstile fields are on one page, one dispatch may reset both (or, historically, the wrong one). Prefer one captcha per page; if you truly need two, inspect the vendor JS for instance scoping first.
  • theme('auto') follows the OS, not Filament. Cloudflare reads prefers-color-scheme; Filament's dark-mode toggle writes a .dark class. A user who flips Filament to dark on a light OS will see a light widget. Either accept it, or listen for Filament's theme toggle and re-init.
  • Language codes are full locales. en-US, not en. There was a fix for "wrong default language format" in v2.3.0 — don't pass app()->getLocale() straight through; map it to Cloudflare's supported list.
  • Hostname restrictions live in the Cloudflare dashboard, not in code. localhost works out of the box; staging/preview domains must be added to the widget or they fail with invalid-domain. First deploy to staging is a classic "it worked locally" moment.
  • CSP. Turnstile loads a script and an iframe from challenges.cloudflare.com. Strict CSP needs it in script-src, frame-src, and connect-src.
  • Debugging order: (1) does the widget appear in the DOM with a cf-turnstile-response entry? (2) does form state contain a token on submit? (3) does your server log an outbound siteverify request? Step 3 is invisible in the browser network tab — it's server-side, so watch storage/logs or Http::fake it.
  • Bus factor. 33 stars, 0 dependents, one maintainer, and a hard Filament-major coupling that forces a package major bump every Filament cycle. Pin the version, keep the Turnstile::make() call isolated in as few places as possible (a fields() helper or trait), and assume that a future Filament upgrade may strand you until a release lands. The escape hatch is cheap: swap the field for raw Cloudflare markup plus coderflex/laravel-turnstile's validation rule — same backend, one layer less.
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.
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc