darvis/livewire-honeypot
darvis/livewire-honeypot provides lightweight honeypot spam protection for Livewire forms, blocking bots without CAPTCHAs or friction for real users. Simple to install and configure, it integrates cleanly with Laravel and existing Livewire components. No JavaScript required. Drop-in protection.
Install the package with Composer:
composer require arviddejong/livewire-honeypot
Start with the package documentation at https://arviddejong.github.io/livewire-honeypot/, then read the v1.6.0 upgrade notes if you already use the package. The first practical use case is protecting a public Livewire form: render the honeypot component inside the form, add the HasHoneypot trait to the Livewire component, and call validateHoneypot() before processing the submission.
Minimal Livewire setup:
<form wire:submit="submit">
<x-honeypot />
<input type="text" wire:model="name" />
<input type="email" wire:model="email" />
<textarea wire:model="message"></textarea>
<button type="submit">Send</button>
</form>
use Livewire\Component;
use ArvidDeJong\LivewireHoneypot\Traits\HasHoneypot;
class ContactForm extends Component
{
use HasHoneypot;
public string $name = '';
public string $email = '';
public string $message = '';
public function submit(): void
{
$this->validateHoneypot();
$this->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email'],
'message' => ['required', 'string'],
]);
// Send the message.
}
}
For a plain Laravel form, use the package's HoneypotService::validate($request->all()) flow as documented. As of v1.6.0 this works correctly in a standard Laravel application where ConvertEmptyStringsToNull is active.
Use the default pattern unless you have a reason to customize it:
<x-honeypot /> inside the <form>.use HasHoneypot; on the Livewire component, not on a form object.$this->validateHoneypot() at the start of the submit action.This keeps the default field name, hp_website, and default error key aligned automatically.
If you bind the honeypot to a different Livewire property, the component and the validation call must use the same path:
<x-honeypot wire:model="contact.hp_website" error-key="contact.hp_website" />
public array $contact = [
'hp_website' => '',
];
public function submit(): void
{
$this->validateHoneypot(
model: 'contact.hp_website',
errorKey: 'contact.hp_website',
);
// ...
}
Initialize the bound property as an empty string. Display the error under the custom key if needed:
@error('contact.hp_website')
<span>{{ $message }}</span>
@enderror
Validate the full request payload, not a filtered subset:
public function store(Request $request, HoneypotService $honeypot)
{
$honeypot->validate($request->all());
$data = $request->validate([
'name' => ['required', 'string'],
'email' => ['required', 'email'],
'message' => ['required', 'string'],
]);
// ...
}
Passing $request->validated() can be a problem because hp_website is usually not part of your validation rules and may be excluded from the validated array. The package expects the documented full payload shape.
Keep the HasHoneypot trait and the validateHoneypot() call on the Livewire component, not on a form object or another non-component class. In v1.6.0, using the trait on a form object throws a LogicException with guidance to move it to the component. This is better than the previous silent behavior where the form object had no honeypot start time and every submission was rejected as spam.
Test the real middleware stack when you want confidence that empty-string/null handling works. Do not default to withoutMiddleware() for the plain-form test path; that bypasses ConvertEmptyStringsToNull and can hide the exact class of bug fixed in v1.6.0.
Useful cases to cover:
hp_website present but null/empty passes.hp_website absent is rejected.hp_website filled is rejected.wire:model path is rejected when the bound property is filled.A component that defines its own validateHoneypot() method keeps working. Use this extension point if you need custom messaging or logging, but preserve the package's core distinction: empty means human, filled means spam, absent means invalid.
v1.6.0 fixes the important distinction between a present-but-null hidden field and a missing hidden field:
null or empty: treated as a normal empty honeypot.This matters because Laravel's ConvertEmptyStringsToNull middleware turns the empty hidden input into null in a default application. Before v1.6.0, that made every plain-form submission look like the field was missing.
If you added this workaround from the 1.5.1 docs:
array_map(fn ($value) => $value ?? '', $request->all())
you can remove it after upgrading. Leaving it in is harmless, but it is no longer needed.
wire:model mismatches are a security issueBefore v1.6.0, validateHoneypot() always read hp_website. If a view used:
<x-honeypot wire:model="contact.hp_website" />
but the component still called $this->validateHoneypot() with defaults, the bound field was not actually checked. Search your views for <x-honeypot wire:model= and make sure each custom binding has a matching call:
$this->validateHoneypot(
model: 'contact.hp_website',
errorKey: 'contact.hp_website',
);
Forms using <x-honeypot /> with no attributes need no change.
If you see the new LogicException, move both the trait and the validateHoneypot() call to the Livewire component. This is most likely to affect applications that put the trait on a Livewire form object. Livewire does not run a mount hook on those objects, so the honeypot never gets a valid start time and every submission is rejected.
An absent hidden field is still rejected after v1.6.0. Avoid wrapping <x-honeypot /> in @if, and do not omit it from forms that call validateHoneypot().
Honeypot fields can be filled by aggressive browser autofill or password managers. If real users report "Spam detected.", inspect the submitted hp_website value before changing validation logic. Keep the field hidden from users and assistive technology according to the package docs.
Work through the likely causes in this order:
<x-honeypot /> actually rendered inside the submitted form?wire:model, does validateHoneypot() receive the same model and error key?v1.6.0 is mostly a bug-fix and security release. Default Livewire forms and default plain-form usage should require no changes beyond removing the optional workaround. Applications using custom wire:model bindings should treat the matching validateHoneypot(model: ..., errorKey: ...) call as required, and applications using the trait on form objects should expect the new LogicException until the trait is moved to the component.
How can I help you explore Laravel packages today?