Roles with Filament Shield
Install Filament Shield and enable Spatie Permission's teams feature:
composer require bezhansalleh/filament-shield
php artisan shield:setup --tenant="App\Models\Team"
php artisan shield:install app --tenant
php artisan shield:generate --all --panel=app
That's all the plugin needs: it detects permission.teams and switches to the SpatieRoleProvider:
- the Members page offers Shield's roles (global ones plus the roles created inside the current team);
- assigning a role syncs the member's Spatie role for that team;
- a
SetPermissionsTeamtenant middleware points Spatie at the current tenant on every request, so$user->can()and Shield's policies answer for the team you're in.
Global roles are templates
Shield's Role resource is tenant-owned, so during a panel request Filament scopes the Role model to the current team and roles with a null team_id are invisible to $user->can(). Treat global roles as templates and give each new team its own copies:
// RegisterTenant page / team factory
$team = Team::create($data);
Teams::roleProvider()->cloneGlobalRolesToTeam($team);
Teams::addMember($team, auth()->user(), Teams::ownerRole());
Your Role model needs the relation Filament uses for scoping:
class Role extends \Spatie\Permission\Models\Role
{
public function team(): BelongsTo
{
return $this->belongsTo(Team::class, 'team_id');
}
}
The same user, different teams
Olivia owns Acme (full access) and is a viewer in Globex — same account, permissions follow the team. Seed a Spatie role named owner with every permission and owners get it automatically; without one, owners still manage members through Teams::canManage().
Gotchas
- Spatie caches permissions with their roles on first use. Call
app(PermissionRegistrar::class)->forgetCachedPermissions()at the end of seeders that create roles. - The default invite role must exist among the team's roles; otherwise the first assignable role is used (
Teams::defaultRoleFor()).