Skip to content

Roles and permissions ​

What it does ​

Decides who can do what, per site. One person can be an editor on one site and a publisher on another. Super admins bypass all of it.

How it works ​

  • Spatie permission, teams mode. teams => true and team_foreign_key => 'site_id' in config/permission.php.
  • Role definitions are global (site_id = null). Assignments are per site, stored in model_has_roles with the site's id.
  • The current site is set on every request by the SetSpatieTeam middleware, registered as persistent tenant middleware on the tenant panel. Persistent is believed to mean it also runs on Livewire's background requests, so permission checks stay correct after the first page load.
  • Outside Filament (tinker, seeders, queues) there is no tenant, so call setPermissionsTeamId($siteId) before checking roles or permissions.
  • Super admin: users.is_super_admin (boolean, not mass-assignable) plus a Gate::before that lets them through every check. Super admins enter every active site without a site_user row.
  • Assigning roles: in /system, the Users resource has a "Site access" repeater that assigns sites and roles. UserSiteAccess does the work.

Permissions and built-in roles ​

Permissions are named area.action.

Permissionsite-adminpublishereditor
page.view, page.create, page.updateyesyesyes
page.deleteyesnono
page.publishyesyesno
post.view, post.create, post.updateyesyesyes
post.deleteyesnono
post.publishyesyesno
package.view/create/update/delete/publishyesnono
media.view, media.createyesyesyes
media.updateyesyesno
media.deleteyesnono
user.manageyesnono
site.updateyesnono
audit.viewyesnono

This is the seeder as of October 2026, including the media.* and audit.view additions. site-admin is synced to the whole permissions array, so it picks up any new permission automatically.

Publisher and editor have no package.* permissions at all. If that isn't intended, add them in RolesAndPermissionsSeeder.

Revisions reuse existing permissions: viewing history needs page.view, restoring needs page.update.

After changing the seeder: php artisan db:seed --class=RolesAndPermissionsSeeder, then php artisan permission:cache-reset.

Files ​

  • config/permission.php: teams on, custom Role model
  • app/Models/Role.php: extends Spatie's role, adds site_assignable and the SYSTEM_ROLES constant
  • database/seeders/RolesAndPermissionsSeeder.php
  • app/Policies/: Page, Package, Post, Site, User, Role (and Media, Activity, PageRevision)
  • app/Services/UserSiteAccess.php
  • app/Filament/System/Resources/{Users,Roles}/
  • Middleware: SetSpatieTeam

Why it's built this way ​

  • Global roles, per-site assignment gives you one central definition of "editor" while letting each person hold different roles on different sites.
  • site_assignable controls which roles a site admin may hand out, so a client can't give someone a platform role.
  • Site and Role policies deny everyone except super admins, so clients can't edit platform configuration.
  • Policies for read-only resources (the audit trail) hard-code create, update and delete to false. The Gate::before bypass would otherwise let a super admin through those checks.