drafolin/filament-inverse-shield
A Filament plugin that inverts the usual Shield permissions workflow, letting your existing Laravel policies and gates drive access control in Filament panels instead of pre-generated permission records. Keeps authorization logic in one place.
drafolin/filament-inverse-shield flips the usual Filament Shield workflow: instead of generating one permission per resource/action and then hand-assigning them to roles, you define roles (and their abilities) up front, and the plugin enforces those roles across your panel's resources, pages and widgets. Day to day, that means less permission-table churn and more "which role owns this screen" thinking.
Minimal path to a working setup:
composer require drafolin/filament-inverse-shield — this is a ^1.4 line; no breaking changes in 1.4.8, so bumping a patch is safe.vendor:publish tags listed in the README, then php artisan migrate.app/Providers/Filament/*PanelProvider.php — the standard Filament v3 shape:
->plugin(\Drafolin\FilamentInverseShield\FilamentInverseShieldPlugin::make())
/admin, and confirm you get a 403 rather than a dashboard. Do this before you build any resources — retrofitting access control onto a half-built panel is where most of the pain lives.Where to look first: the published config (it drives the role/ability mapping and guard selection), then the plugin class, then your User model — the package expects the role relationship to be reachable from the authenticatable model.
Role-first modelling. Decide your role taxonomy before writing resources: something like super_admin, admin, editor, viewer. Each resource then falls into a role's bucket, and adding a new resource becomes "which role should see this?" rather than "which twelve permissions do I create?"
Seeder-driven role definitions. Keep role definitions in code (a seeder or a small config array) and run it as part of deploy. This is exactly what 1.4.8 touches: previously the seeder could clobber attributes on an existing admin role; now all attributes are preserved on re-seed. Practically, this makes re-running the seeder in a deploy hook safe and idempotent — you can add it to your release script without fearing it will reset customised role metadata every deploy.
Per-panel guards. If you run multiple panels (e.g. admin and app), set the guard per panel in config rather than relying on the default web guard. Mixed guards are the single most common cause of "the user has the role but nothing is allowed."
Custom pages and widgets. Package enforcement is strongest on resources it can resolve to a model. For custom Filament Pages and dashboard widgets, add public static function canAccess(): bool (pages) or canView() (widgets) and delegate to the same role check the plugin uses. Don't assume registration alone protects them.
Fine-grained overrides. Where a role needs a narrower slice than "full CRUD on this resource", drop a policy in app/Policies — Laravel's policy resolution wins, and the plugin's coarse role gate stays as the outer boundary.
Testing. Use RefreshDatabase plus your role seeder, then actingAs($user) and assert assertForbidden() / assertOk() per resource page. One test per role per panel catches almost every regression when a new resource is added without an access decision.
php artisan permission:cache-reset (or cache:forget spatie.permission.cache) in deploy. Symptom is "the role exists in the DB but the UI still says no."canAccessPanel() is not optional. Filament lets any authenticated user into a panel unless the model implements canAccessPanel(). The plugin gates resources, not the panel shell. A user with zero roles can still land on an empty dashboard if you skip this.Gate::before super-admin bypass. If you register a Gate::before for super admins, it bypasses everything, including your custom policies. Convenient, but it hides bugs during development — test as a non-super-admin.php artisan filament:optimize-clear (and optimize:clear in CI) after adding resources/pages, otherwise navigation and access decisions lag behind your code.$user->getRoleNames() and $user->roles->pluck('guard_name') — if the guard doesn't match the panel's auth guard, nothing else matters. Check the published config's guard setting first.bezhansalleh/filament-shield. They solve the problem from opposite directions and will fight over policies and permission naming. Pick one.How can I help you explore Laravel packages today?