Filament v5 is the version that finally makes "build the admin panel" a one-evening task instead of a two-week project. Released alongside Laravel 12, it ships with rebuilt forms and tables, native multi-tenancy, and a plugin ecosystem that genuinely covers the long tail. This guide is what I wish someone had handed me on day one the patterns that scale to ten resources, fifty, and beyond.
What this guide covers
- Installing Filament v5 cleanly on a fresh Laravel 12 app
- Designing your first Resource with relations, filters, and bulk actions
- Role-based access with Spatie Permissions
- Multi-tenancy: tenant-scoped resources without query leaks
- Theming, custom widgets, and a deployment checklist
Info
Filament v5 in one sentence
A Livewire-powered admin panel framework that gives you tables, forms, and dashboards in a few PHP classes — no separate frontend, no API, no JSON contract to maintain.
1. Installation that actually works on day 30
composer require filament/filament:"^5.0" -W
php artisan filament:install --panels
php artisan make:filament-user
The default panel lives at /admin. Before you ship anything, lock it down.
// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->authGuard('web')
->colors(['primary' => Color::Teal])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([Authenticate::class, EnsureUserCanAccessFilament::class]);
}
Warning
Default access policy is too permissive
Filament out of the box lets any authenticated user reach <code>/admin</code>. Add an <code>EnsureUserCanAccessFilament</code> middleware that checks <code>$user->is_admin</code> or a Spatie role. Without it, your customer support team logs in to the admin panel by accident.
2. Your first Resource : done right
php artisan make:filament-resource Project --generate
// app/Filament/Resources/ProjectResource.php
public static function form(Form $form): Form
{
return $form->schema([
Forms\Components\Section::make('Project details')
->columns(2)
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(120)
->live(onBlur: true)
->afterStateUpdated(fn ($set, $state) => $set('slug', Str::slug($state))),
Forms\Components\TextInput::make('slug')
->required()
->unique(ignoreRecord: true),
Forms\Components\Select::make('owner_id')
->relationship('owner', 'name')
->searchable()
->preload()
->required(),
Forms\Components\Select::make('status')
->options(['draft' => 'Draft', 'active' => 'Active', 'archived' => 'Archived'])
->default('draft')
->required(),
Forms\Components\RichEditor::make('description')
->columnSpanFull()
->disableToolbarButtons(['attachFiles']),
]),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')->searchable()->sortable(),
Tables\Columns\TextColumn::make('owner.name')->label('Owner')->sortable(),
Tables\Columns\BadgeColumn::make('status')
->colors(['gray' => 'draft', 'success' => 'active', 'warning' => 'archived']),
Tables\Columns\TextColumn::make('updated_at')->since()->sortable(),
])
->filters([
Tables\Filters\SelectFilter::make('status')->options(['draft', 'active', 'archived']),
Tables\Filters\Filter::make('mine')
->toggle()
->query(fn ($query) => $query->where('owner_id', auth()->id())),
])
->actions([Tables\Actions\EditAction::make(), Tables\Actions\DeleteAction::make()])
->bulkActions([Tables\Actions\DeleteBulkAction::make()])
->defaultSort('updated_at', 'desc');
}
Pro tip
Always pass <code>ignoreRecord: true</code> on unique validators inside Resources without it, editing a record fails because Filament re-validates the existing slug against itself.
3. Role-based access with Spatie Permissions
composer require spatie/laravel-permission
// app/Filament/Resources/ProjectResource.php
public static function canViewAny(): bool { return auth()->user()->can('view-any project'); }
public static function canCreate(): bool { return auth()->user()->can('create project'); }
public static function canEdit($record): bool { return auth()->user()->can('update', $record); }
public static function canDelete($record): bool { return auth()->user()->can('delete', $record); }
Wire the same permissions into your model policies and you get one source of truth, Filament inherits the policy automatically when you implement canEdit via the Gate.
4. Multi-tenancy without leaks
Filament v5 has first-class tenancy. Tell the panel which model represents a tenant and every resource is scoped automatically.
// In AdminPanelProvider::panel()
->tenant(Workspace::class, slugAttribute: 'slug')
->tenantRegistration(RegisterWorkspace::class)
->tenantProfile(EditWorkspaceProfile::class)
// app/Models/Workspace.php
public function getFilamentName(): string { return $this->name; }
public function canAccessFilament(): bool { return $this->users->contains(auth()->user()); }
// app/Models/User.php
public function getTenants(Panel $panel): Collection
{
return $this->workspaces;
}
public function canAccessTenant(Model $tenant): bool
{
return $this->workspaces->contains($tenant);
}
Danger
Tenant scoping is opt-in per resource
Resources that have a direct relationship to the tenant model auto-scope. Resources that go through pivot tables or polymorphic relations need explicit query overrides. Audit every Resource: open the table, switch tenants, confirm zero rows leak. Do this <em>before</em> any customer sees the panel.
5. Custom widgets that earn their keep
// app/Filament/Widgets/ProjectsOverview.php
class ProjectsOverview extends BaseWidget
{
protected function getStats(): array
{
$tenant = Filament::getTenant();
return [
Stat::make('Active projects', $tenant->projects()->where('status', 'active')->count())
->description('Last 30 days')
->descriptionIcon('heroicon-o-arrow-trending-up')
->color('success'),
Stat::make('Open tasks', $tenant->tasks()->whereNull('completed_at')->count())
->color('warning'),
Stat::make('Avg. response time', '4h 12m')
->color('primary'),
];
}
}
less time spent on CRUD vs hand-rolling Blade + Tailwind from scratch
✓ Pros
- Resources cover 80% of admin needs out of the box
- First-class multi-tenancy and RBAC
- Plugin ecosystem (Shield for permissions, Spatie media library, etc.)
- Looks polished without designer involvement
✕ Cons
- Customising deeply means writing Livewire fight or learn the framework
- Bundle size is non-trivial; ship Filament behind auth, never on public pages
- Octane compatibility has rough edges; test before enabling
Production checklist
Lock the panel behind a middleware
Even on staging. The number of leaked admin panels indexed by Google is depressing.
Cache permissions
<code>spatie.permission.cache</code> on Redis. Without it, every page load hits the permissions table.
Run `php artisan filament:optimize` on deploy
It precompiles Filament assets saves roughly 200ms on cold cache.
Set up tenant tests
A single Pest test that creates two tenants and asserts zero cross-leak prevents the worst class of bugs.
Success
You will reach for Filament more than you expect
Once a team ships their first panel they tend to consolidate every internal tool into Filament settings, billing review, customer support, audit logs. That is a feature, not a bug.