solution-forest/filament-access-management
Filament Access Management provides a complete roles and permissions UI for Filament admin panels. Manage roles, assign permissions and users, and control access to resources and pages with an easy-to-use interface built on Spatie's permission package.
Install and wire it up (about 10 minutes):
composer require solution-forest/filament-access-management
php artisan vendor:publish --tag="filament-access-management-config"
php artisan vendor:publish --tag="filament-access-management-migrations"
php artisan migrate
spatie/laravel-permission, so your User needs Spatie\Permission\Traits\HasRoles and, for the panel, Filament\Models\Contracts\FilamentUser with a canAccessPanel() method.AdminPanelProvider:
->plugin(\SolutionForest\FilamentAccessManagement\FilamentAccessManagementPlugin::make())
config/filament-access-management.php. The keys you care about are the user_model, role_model and permission_model bindings, plus whatever permission-name/panel settings it exposes. This file is the source of truth for every "why is this denied?" question later.Where to look when something is off: the generated roles / permissions / model_has_roles tables, then the plugin's RoleResource, then your User model's traits.
Let the package own the permission catalogue, not your seeders. The permission names are derived from Filament Resource/Page class names, so the intended workflow is: build resources → open the Roles screen (or run the package's sync) → tick boxes. Hard-coding Permission::create(['name' => 'view_any_user']) in a seeder works until a resource is renamed and you get a ghost permission plus a 403.
Seed roles, not permissions. In RoleSeeder, look the role up by name with the correct guard and use syncPermissions() with names pulled from the DB or from the package's generated set — never duplicate the naming logic.
$role = Role::findOrCreate('editor', 'web');
$role->syncPermissions(Permission::where('name', 'like', '%PostResource%')->get());
Super-admin bypass with Gate::before. Do not build a "has every permission" role — it goes stale the moment you add a resource.
Gate::before(fn ($user) => $user->is_super_admin ? true : null);
Keep authorization in policies. The package controls which role has which permission; policy methods still decide what that permission means. Filament asks viewAny, create, update, delete, deleteAny against the resource's model — a PostPolicy with those methods is what actually makes the ticks in the UI do something. For custom Pages and Widgets, use canView() / canAccess() returning auth()->user()->can(...).
Multi-panel / multi-guard. Each panel has a guard; roles are guard-scoped in spatie. If you run an admin panel and an app panel, decide on one guard per panel and create roles under that guard, or you will silently assign a web role to a user authenticating on admin. For Filament multi-tenancy, spatie's teams feature (teams => true, team_id on roles) is the right lever, not duplicating roles.
Testing. Reset the permission cache in setUp() and use actingAs:
protected function setUp(): void {
parent::setUp();
app(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();
}
Then assert $this->actingAs($editor)->get(PostResource::getUrl())->assertOk() and the inverse 403. Filament's own assertCanSeeTableRecords / Livewire tests compose fine on top.
Deploy. Run the permission sync step in your deploy script after migrations and before php artisan permission:cache-reset, otherwise the first requests after deploy serve a stale cached permission map.
web while the panel uses admin produce a user whose can() is always false. Check config/auth.php guards and spatie's config/permission.php defaults.guard.php artisan permission:cache-reset (or forgetCachedPermissions() in code). "It worked, then I redeployed and everyone is locked out" is almost always this.Permission::where('name', 'like', 'App\\\\Filament\\\\Old%')->delete().role_model / permission_model at your own Eloquent models, they must extend spatie's Role/Permission and keep the same table names — and the HasRoles trait on User must resolve those classes via config/permission.php, not the package config alone. Change both.migrate on an existing app.FilamentUser contract is mandatory. Even with perfect roles, a User that doesn't implement FilamentUser (or canAccessPanel()) gets bounced from the panel in non-local environments — it looks like a permission bug and isn't.viewAny and withhold create/update rather than hiding the resource.vendor:publish --tag=... — check the package's available tags) to relabel the grouped permission tree, and subclass RoleResource if you need extra fields (e.g. a description column) without forking.composer.json constraint pins the Filament version you're on before upgrading either side, since a Filament major bump changes how permissions are enumerated.How can I help you explore Laravel packages today?