Filament v5 ships a native register page. Some plugins ignore it and roll their own controller. Here's how we added captcha, honeypot, role assignment, and event bridging — entirely through Filament's documented hooks, without touching register() itself.
Filament is one of the best backend frameworks for Laravel. It builds admin panels fast, looks great out of the box, and is deeply extensible. But most non-trivial projects don't stop at the backend. If you're building a SaaS, a web app with a public-facing portion, or anything with real users signing up, you eventually need authentication that lives outside the admin shell.
A common answer is to reach for one of Laravel's official starter kits. The modern lineup — React, Vue, and Livewire flavours, all with auth scaffolding baked in — is the path most teams take today, and earlier kits like Breeze and Jetstream from the Laravel 10.x era are still maintained and widely deployed. They're battle-tested, beautifully documented, and a great fit for many projects: they handle public-facing auth completely while letting Filament focus on the admin side. For plenty of teams that separation is exactly what you want, and these kits absolutely deserve their place in the Laravel ecosystem.
That said, if you'd rather harden your authentication and roll your own using Filament, you don't need to reach outside the framework — Filament v5 already has everything you need to extend. It ships a complete auth flow — \Filament\Auth\Pages\Login, \Filament\Auth\Pages\Register, email verification, password reset, and multi-factor authentication — and these aren't just admin-panel utilities. They're real, themable, extensible pages you can put in front of your users.
The catch: the register page in particular needs work before it's production-ready. Out of the box there's no captcha, no honeypot, no role assignment, no Laravel-event bridge for listeners that other parts of your app already depend on. Most plugins solve this by reaching for a custom Laravel controller, a Blade form, and manual validation — abandoning Filament's auth flow entirely.
We took the other approach while building tallcms/filament-registration(Link). Every production feature — captcha, honeypot, rate limiting, default role assignment, post-registration redirect — landed inside Filament's documented extension points. We never overrode register(). These patterns apply to any Filament v5 project that needs a public register page without leaving the framework.
The Core Idea: Two Hooks, Not One Override
Filament's Register page exposes two extension points :
mutateFormDataBeforeRegister(array $data): array — runs after validation but before User::create(). The place for anything that should gate registration.
handleRegistration(array $data): Model — wraps user creation. The place for anything that should run after the user exists.
The temptation, especially if you're porting from a legacy controller, is to override register() and rebuild everything inside one method. Don't. Filament's register() already orchestrates throttling, validation, email verification, response dispatch, and event firing. You won't get all those right by rewriting it. You don't need to.
class Register extends \Filament\Auth\Pages\Register
{
public function form(Schema $schema): Schema
{
return $schema->components([
$this->getNameFormComponent(),
$this->getEmailFormComponent(),
$this->getPasswordFormComponent(),
$this->getPasswordConfirmationFormComponent(),
HoneypotField::make(),
CaptchaField::make(),
]);
}
protected function mutateFormDataBeforeRegister(array $data): array
{
$this->checkHoneypot($data);
$this->throttleCaptcha();
$this->verifyCaptcha($data);
unset($data[$this->honeypotField], $data[$this->tokenField]);
return $data;
}
protected function handleRegistration(array $data): Model
{
$user = parent::handleRegistration($data);
$this->maybeMarkEmailVerified($user);
$this->maybeAssignDefaultRole($user);
event(new \Illuminate\Auth\Events\Registered($user));
return $user;
}
}
That's the entire shape. Everything below is what goes inside those methods.
Layer 1: Honeypot via Validation, Not Stealth
The legacy approach to honeypots is to silently render a fake success page when the field is filled, hoping the bot logs a false positive. It works, until it doesn't — modern bots roll their own success-page detectors.
A cleaner approach inside Filament: throw a ValidationException. Filament catches Laravel's validation exceptions automatically and surfaces the message on the form. Bots that don't fill the honeypot get through validation; bots that do see a generic "Bot check failed" message attached to the honeypot field.
if (! empty($data[$this->honeypotField])) {
throw ValidationException::withMessages([
$this->honeypotField => 'Bot check failed. Please try again.',
]);
}
Layer 2: Rate-Limit Before You Verify
Layer 3: Pluggable Captcha via a Contract
Layer 4: Two-Layer Config (Env + Admin UI)
Layer 5: Container-Bound Redirects
We have shared this in details on this blog
Top comments (0)