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 Spatie Roles Permissions Laravel Package

althinect/filament-spatie-roles-permissions

Filament plugin built on Spatie's Laravel Permission package that provides admin resources for managing roles and permissions, automatic permission and policy generation, and support for teams. Requires Spatie migrations and the HasRoles trait on your User model.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and wire up (5 minutes)

composer require althinect/filament-spatie-roles-permissions

# Spatie's own migrations are a prerequisite — this package ships none
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate

# Package config (note: the README uses two different filenames for it)
php artisan vendor:publish --tag="filament-spatie-roles-permissions-config"
php artisan vendor:publish --tag="filament-spatie-roles-permissions-translations" --force

2. Register the plugin in your panel provider:

use Althinect\FilamentSpatieRolesPermissions\FilamentSpatieRolesPermissionsPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->id('admin')
        // ...
        ->plugin(FilamentSpatieRolesPermissionsPlugin::make());
}

3. Add the trait to App\Models\User:

use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
}

4. Where to look first

  • config/filament-spatie-roles-permissions-config.php — this is where nearly every customization lives (guard visibility, column span, modal behaviour, user name column, permission generation targets).
  • The two auto-registered resources: Roles and Permissions under your navigation. Create a role like Super Admin or Editor there first, then assign it to yourself to confirm the flow before touching policies.
  • Your UserResource — add a roles select so admins can actually assign what you just created:
Forms\Components\Select::make('roles')
    ->relationship('roles', 'name')
    ->multiple()
    ->preload()
    ->searchable(),

First real use case: create permissions from your models automatically, then gate a resource with them.

php artisan permissions:sync

If you have a Post model this generates view-any Post, view Post, create Post, update Post, delete Post, restore Post, force-delete Post, replicate Post, reorder Post. Assign those to a role in the Permissions UI, and the RBAC loop is closed.


Implementation Patterns

Bootstrapping role assignment for a fresh project

The chicken-and-egg problem: you need a role to see the Roles resource, but the resource is what creates roles. Seed it instead of clicking:

// database/seeders/RoleSeeder.php
$admin = Role::create(['name' => 'Super Admin', 'guard_name' => 'web']);
$admin->syncPermissions(Permission::all());

User::where('email', '[email protected]')->first()->assignRole($admin);

Super Admin bypass

Rather than granting every permission explicitly, use the shipped trait plus a Gate::before:

use Althinect\FilamentSpatieRolesPermissions\Concerns\HasSuperAdmin;

class User extends Authenticatable
{
    use HasRoles, HasSuperAdmin;
}
// AppServiceProvider::boot()  (Laravel 11+ — no AuthServiceProvider)
Gate::before(function (User $user, string $ability) {
    return $user->isSuperAdmin() ? true : null;
});

Return null, never false, for non-super-admins — false short-circuits the whole gate chain and breaks everyone else.

Hiding the Roles/Permissions resources from non-admins

The plugin's resources go through Laravel policies. Author two policies and register them:

// AppServiceProvider::boot()
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;

Gate::policy(Role::class, \App\Policies\RolePolicy::class);
Gate::policy(Permission::class, \App\Policies\PermissionPolicy::class);
class RolePolicy
{
    public function viewAny(User $user): bool
    {
        return $user->can('view-any Role');
    }
    // view, create, update, delete...
}

This is the idiomatic way to remove them from the nav — don't override shouldRegisterNavigation().

Generating policies alongside permissions

php artisan permissions:sync -P          # create missing policies, leave existing alone
php artisan permissions:sync -O          # overwrite existing policy classes
php artisan permissions:sync -COPY -Y    # policies + overwrite + yes-to-all prompts

-COPY without -Y will prompt per file. Reserve it for scaffolding new projects; on a live codebase it silently wrecks hand-written policy logic (before(), ownership checks, viewAny overrides).

Guard-aware setup

Any panel that isn't the web guard needs it declared in config/auth.php, and you must keep guard_name consistent between roles, permissions, and the model. In the UI, toggle the guard field via should_show_guard in the package config so admins don't create roles on the wrong guard.

Teams / multi-tenancy

This is the most fragile area. Sequence:

  1. config/permission.php → 'teams' => true, correct team_model and team_foreign_key.
  2. Subclass Spatie\Permission\Models\Role (and optionally Permission), point the config at your subclass, and add a team() BelongsTo on both.
  3. Register the tenant middleware:
use Althinect\FilamentSpatieRolesPermissions\Middleware\SyncSpatiePermissionsWithFilamentTenants;

$panel->tenantMiddleware([
    SyncSpatiePermissionsWithFilamentTenants::class,
], isPersistent: true)
  1. The stock relationship('roles', 'name') select will not write team_id into the pivot. Use the documented override:
Select::make('roles')
    ->relationship(name: 'roles', titleAttribute: 'name')
    ->saveRelationshipsUsing(function (Model $record, $state) {
        $record->roles()->syncWithPivotValues($state, [
            config('permission.column_names.team_foreign_key') => getPermissionsTeamId(),
        ]);
    })
    ->multiple()
    ->preload()
    ->searchable(),

Skipping step 4 gives you the classic bug: role assigned successfully, then the user has no permissions because the pivot row belongs to team null.

Checking permissions in Filament resources and pages

public static function canViewAny(): bool
{
    return auth()->user()->can('view-any Post');
}

// Blade
@can('update Post')
    <x-filament::button ... />
@endcan

Since the generated names are space-separated and capitalised (view-any Post), keep a constants class or enum so you don't fat-finger strings across resources:

final class Permissions
{
    public const POST_VIEW_ANY = 'view-any Post';
}

Config-driven UI tweaks worth knowing

Config key Effect
should_show_guard Hide the guard select when you only have one guard
column_span / full-width Restores v3 full-width layout under custom themes
redirect-to-index Send users to the list after save instead of the view page
simple modal resource Lighter create/edit modals for roles/permissions
remove empty state actions Hide the "create" CTA on empty tables
user_name_column Column used for user record titles in relationship pickers

Gotchas and Tips

Publishing config on update is destructive. The README explicitly tells you to run vendor:publish --tag="filament-spatie-roles-permissions-config" --force after composer update — and warns it overrides your settings. In real projects: back up or diff the config, or don't publish at all until you actually need to change something. Committing the published config to VCS is your safety net; the --force flow will otherwise wipe guard/column settings in a single CI run.

--clean and --hard break role assignments. permissions:sync -C deletes all permissions (dropping their role_has_permissions pivot rows). -H truncates and resets auto-increment. Both are fine locally, both are an outage in production. Never put permissions:sync in a deploy pipeline without -P being idempotent-safe and with the clean flags omitted.

Spatie caches aggressively. After seeders, config changes, or direct DB edits, permissions appear not to exist. Fix:

php artisan permission:cache-reset
# or, since Filament caches too
php artisan optimize:clear

Wire permission:cache-reset into your deploy script.

Guard mismatch is the #1 silent failure. Permission created on web, user authenticating on admin guard → can() always false, no error. Check guard_name in roles and permissions tables before debugging anything else.

The permissions UI shows all permissions. A role editor can grant themselves anything. Restrict the Permissions resource with a policy, or at minimum gate PermissionPolicy::create/update behind the Super Admin check.

Permission names are generated from model basenames, not table names. A BlogPost model yields view-any BlogPost. Rename a model and you now have orphaned permissions plus new ones — old role assignments quietly stop matching. Diff the permission list after any model rename.

permissions:sync won't delete stale entries for removed models. Combine with -C (in dev only) or prune manually.

Scope mismatch vs Filament Shield. This package manages roles/permissions and generates model policies — it does not generate per-page or per-widget permissions the way Shield does. If you need "can this user see the Dashboard page widget X," plan to hand-roll canView() on those classes or reach for Shield.

Laravel 11+ has no AuthServiceProvider. Both the Gate::policy() registration and the Gate::before() super-admin hook go into AppServiceProvider::boot(). Tutorials referencing AuthServiceProvider are stale.

Resource discoverability. Resources are registered by the plugin, so php artisan make:filament-resource won't show them and you can't just edit them in place. To customize heavily (extra fields, custom table columns), extend the shipped resource classes and register your subclasses, or set the cluster/navigation group via the config — cluster support was added explicitly in v2.2.25.

Translations need --force and a locale set. If the UI renders raw keys (filament-spatie.role), you published without --force or you're missing the package's lang files for your locale.

Debugging checklist when a user "has the role but still can't do anything": (1) permission:cache-reset; (2) check guard_name; (3) for teams, verify the pivot's team_id matches getPermissionsTeamId(); (4) confirm Gate::before returns null not false; (5) confirm the permission string matches exactly, including the space and capitalisation.

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.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle