blendbyte/livewire-honeypot
Blendbyte Livewire Honeypot provides lightweight, CAPTCHA-free spam protection for Laravel Livewire forms. It uses hidden honeypot fields and timing checks to catch automated submissions, helping keep forms simple, accessible, and user-friendly for real visitors.
composer require blendbyte/livewire-honeypotphp artisan vendor:publish --tag=honeypot-config, then read config/honeypot.php first — it is the single place that governs fill-time thresholds, expiry, and logging. Most "why did this get flagged?" questions are answered there.<form wire:submit="submit">
<input type="text" wire:model="name">
...
<x-honeypot />
<button type="submit">Send</button>
</form>
Before upgrading to 2.2.0, read docs/upgrading.md#upgrading-to-22. If you maintain a browser test suite, budget time for the selector change described below — it is the one thing in this release that will break CI rather than production.
Livewire forms (the default path). Add <x-honeypot /> inside the component's Blade view and keep the validation/submit() logic unchanged. The package rejects silently — no exception to catch, no request fired — so your normal success path is untouched.
Plain HTML forms (new in 2.2.0). <x-honeypot /> now works outside Livewire, posting to a controller with a signed token. See docs/plain-forms.md. This is the pattern to reach for when you have a marketing-site form, a Laravel Breeze/Fortify-style login page, or any Blade form that posts without a Livewire component behind it. Verify on the server in the controller (or via middleware) before persisting.
Per-form tuning. Don't reach for one global threshold. A login form and a long application form have completely different legitimate fill times. Publish the config, keep the default for auth/contact, and override HONEYPOT_MAXIMUM_FILL_SECONDS (or disable expiry with 0) for the slow ones: multi-step wizards, checkout with an address lookup, anything with file uploads, saved drafts, or a "review your details" page users leave open.
Expiry as a first-class outcome. With 2.2.0, expiry is a distinct reason (form_expired) rather than a generic failure. Lean into it:
if ($reason === 'form_expired') {
return back()->with('status', 'This form has expired — please reload and try again.');
}
Showing the shipped "This form has expired" message is usually right; a generic "something went wrong" just generates support tickets.
Testing. HoneypotService::fake() now resets itself between tests, so you can call it in setUp()/beforeEach without leaking state into unrelated tests. Conversely, if you were relying on a globally-faked honeypot persisting across the suite, move the call into the tests or a per-test hook that runs each time.
Logging while tuning. Turn on HONEYPOT_LOG_VALUE in staging for a week after rollout. It tells you whether rejections are bots or real users tripping autofill. Turn it off in production — it writes user-entered content to logs.
Layering. Keep this alongside rate limiting and, if abuse is targeted, a CAPTCHA on the highest-value endpoints. A honeypot stops drive-by bots cheaply; it is not a defence against a determined attacker.
HONEYPOT_MAXIMUM_FILL_SECONDS, or every legitimate submission after the minimum is reached is now rejected as expired. Grep your config and .env for the fill-time keys before deploying.form_expired is a new rejection reason. Any match/switch over reasons, analytics event mapping, or test assertion that expects a fixed set of reasons needs a new arm. Assertions like assertSame('spam', $reason) may now fail on slow tests.wire:model. Bait field names are now generated per form (referral_3f9a), so $browser->type('name', ...) or any selector keyed on the old field name is dead. This is the most likely source of red CI.token_min_length is deprecated and ignored. Remove it from your config and delete any test that asserts on it now — it does nothing in 2.2.0 and disappears in 3.0.display:none, hidden, or visibility:hidden makes it invisible to the accessibility tree and to the naive bots the package is targeting, while also risking a11y tooling flags. Leave the shipped styles alone.HONEYPOT_MAXIMUM_FILL_SECONDS=0 disables it entirely. Use it for anything where a user legitimately returns to a stale tab — otherwise you'll reject real people who left a form open over lunch.@cache), the plain-form token goes stale or, worse, is shared across users. Exclude those pages or make the form Livewire-based.failed() hook for this. If you need observability, hook the event/analytics layer rather than wrapping submit() in try/catch; there will be nothing to catch.fake()/real() is the debugging lever. If a form is misbehaving and you suspect the honeypot, call HoneypotService::fake() in a tinker session or a throwaway route to prove the rejection is coming from here before you go hunting elsewhere.How can I help you explore Laravel packages today?