Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Shield Laravel Package

bezhansalleh/filament-shield

Shield adds complete access management to Filament panels: resource, page, and widget permissions, custom permissions, and automatic policy generation, with per-resource policies and seamless role integration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up in three steps (about five minutes).

composer require bezhansalleh/filament-shield
php artisan vendor:publish --tag="filament-shield-config"
php artisan shield:setup          # interactive: publishes migrations, picks guard, offers super-admin, runs generate

Then make sure your auth model implements Spatie's contract:

// config/filament-shield.php
'auth_provider_model' => 'App\\Models\\User',

// app/Models/User.php
use Spatie\Permission\Traits\HasRoles;

Finally register the plugin on each panel so admins get the Roles/Permissions UI:

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugin(ShieldPlugin::make());
}

Where to look first, in this order:

  1. config/filament-shield.php — this file is the package. Every behaviour (permission key casing, policy paths, method lists, super admin, tenancy) is a config key. Read it top to bottom before writing any code.
  2. The generated app/Policies/* — see exactly what Shield produced for your resources.
  3. The Shield resource inside your panel — the fastest way to confirm permissions were actually discovered (/admin/shield/roles).

First real use case: you have 30 resources and no authorization. Run php artisan shield:generate --all, open the Roles resource, create a "Editor" role, tick ViewAny:Post, View:Post, Create:Post, Update:Post, and assign it to a user. No policy code written by hand, and PostPolicy now returns true/false correctly because Shield generated the policy methods that map to those permission names. That loop — generate → tick boxes → assign — is the 90% path.

Implementation Patterns

Treat permission generation as a build step, not a one-off. New resource? Add it to your workflow:

php artisan shield:generate --all --no-interaction
php artisan shield:seeder --no-interaction   # roles + direct permissions as a class

Commit both the generated policies and the generated seeder. The seeder is the artifact that keeps staging/production in sync with the code; running shield:generate directly against production is a smell because it mutates the permission table without review. Instead, ship the seeder and run it in your deploy pipeline (php artisan db:seed --class=ShieldSeeder).

Decide your permission key convention on day one. separator (:) + case (pascal) produce ViewAny:Post. Changing this later renames every permission row and silently orphans every role→permission pivot. If your team has naming standards, set it now:

'permissions' => [
    'separator' => '.',
    'case'      => 'snake',   // view_any.post, create.post
    'generate'  => true,
],

Override key composition for colliding resource names. Two CategoryResource classes handling Blog\Category and Shop\Category both want ViewAny:Categories. Use the facade hook in AppServiceProvider::boot() and return null for everything else so the default builder still applies:

FilamentShield::buildPermissionKeyUsing(
    function (string $entity, ?string $affix, string $subject, string $case, string $separator) {
        if ($entity === 'custom') {
            return $subject;      // IdP-managed keys, use verbatim
        }

        return null;              // default builder
    }
);

Custom (ad-hoc) permissions for non-Filament actions. Anything outside a panel — "export reports", "impersonate user", API-only abilities — belongs in the config's custom permissions list. They show up in the same checkbox UI, so a role is defined in one place instead of half in Shield and half in a bespoke admin screen. For keys owned by an external identity provider (Keycloak, Auth0), mark them externally managed so Shield never tries to create or rename them.

Super admin as a gate interception, not a role. For "the owner can do anything", prefer the gate interception so new permissions don't require re-ticking boxes on the super-admin role every time you add a resource. Reserve the role-based approach when you need an auditable, enumerable list of what the super admin technically holds. Keep at least one non-super-admin test account — with interception on, every policy returns true and your authorization tests become meaningless.

Pages and widgets need explicit enforcement. Resources get it for free via the generated policy; pages and widgets merely get permissions generated. Check the "Permission Enforcement" section and wire the check yourself (visibility/canAccess-style guard) — the classic bug is a custom dashboard widget that renders for everyone while a perfectly good View:StatsWidget permission sits unused in the DB.

Policies: generate once, then own them. Use policies.merge => true so custom methods survive regeneration, and set policies.generate => false globally (or exclude specific resources via the resources config) once a policy has real business rules — e.g. "editors only update their own posts". Generated policies are a scaffold; the moment you add tenancy scoping or ownership checks, regenerating over them is data loss.

Multi-tenancy. Enable Spatie teams before the first shield:setup (the migration needs the team foreign key column), then configure Shield's tenancy options so roles are per-team. Retrofitting teams after roles exist means migrating every pivot row by hand.

Testing pattern. In Pest/PHPUnit, seed your Shield seeder in setUp, then actingAs($editor)->get(PostResource::getUrl())->assertOk() and assert a 403 for a role missing the permission. This catches the far more common real-world failure: a permission exists but was never granted, or was granted to a role that isn't attached.

Gotchas and Tips

  • 4.x is a full rewrite and not backward compatible with 3.x/4.x-beta. Read the Upgrade section before composer update. The config file shape, command names, and policy generation all changed. Budget a half-day, not ten minutes.
  • Separator/case conflicts throw immediately. _ with snake/lower_snake/upper_snake, or - with kebab, raises InvalidArgumentException — the affix and subject become indistinguishable. Pick a separator outside the case's own delimiter set.
  • Renaming permissions breaks role grants silently. Change case, separator, resources.subject, or a permission-key callback after go-live, and every existing role_has_permissions row points at a dead key. Roles quietly lose access, and no error is raised. Either lock the convention early or write a migration that rewrites the name column.
  • Policy placement is per-model and surprising. Models under app/Models land in app/Policies (nesting preserved). Vendor models land flat in app/Policies — two vendor models with the same basename will collide. Models in a non-app/Models Models directory get a sibling Policies directory, which Laravel's default discovery may not find; you must register them (Gate::guessPolicyNamesUsing or the $policies array). Check the placement table and verify with Gate::getPolicyFor($model) rather than assuming.
  • Spatie's permission cache will lie to you. After changing permissions or role assignments outside a request cycle (seeder, tinker, migration), run php artisan permission:cache-reset (or optimize:clear). "The permission is in the DB but can() returns false" is almost always this.
  • Direct user permissions are a debugging trap. The seeder can grant permissions directly to users as well as via roles. A user who "shouldn't have access" often has one stray direct permission. Audit with $user->getDirectPermissions() before touching policies.
  • Multi-panel discovery bleeds across panels. With entity discovery across all panels enabled, panel B's Roles UI lists panel A's resources. Scope generation per panel (--panel=admin) or disable global discovery; otherwise you're ticking boxes for screens the admin will never see.
  • Prohibited commands exist for a reason. Shield blocks destructive commands in certain environments. Deploy scripts must use non-interactive flags (--no-interaction, --all, explicit --option=policies / --option=permissions) and expect a refused command rather than a hung prompt in CI.
  • Non-interactive shield:setup needs its flags. It's smart and interactive; in a Docker build it will block forever. Publish config via env-specific config files instead, and reserve shield:setup for local/dev.
  • Localization is opt-in. Permission labels default to English; generate the translation files if your panel is localized, otherwise you get a mixed-language UI. Note the labels are keyed by permission name — renaming keys means regenerating translations too.
  • Extension points worth knowing: the FilamentShield facade (permission catalogue retrieval, key builder), the publishable Shield resource (for layout/navigation/labels/global-search changes — publish rather than fork), and the resources/pages/widgets config arrays (subject, manage, exclude) which are the surgical way to keep a noisy or internal resource out of the permission UI without writing code.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
thomas-0816/doctrine-dbal-duckdb
moyisoft/laravel-admin
oxygen/mod-pages
oxygen/mod-import-export
oxygen/mod-media
ksoft/laravel-contentbuilderjs
ourgoldpackages/exchange
mikkimike/laravel-exchange1c
wandesnet/mercadopago-laravel
tsfcorp/email
larafony/core
laravel-enso/currencies
tartan/laravel-online-payment
gam/laravel-sat-catalogs
cloudcreativity/laravel-stripe
abdulelahragih/querybuilder
wedevbr/celcoin-laravel
surazdott/nepali-date
devsquad-cockpit/laravel
coding-socks/laravel-upload-handler