Roles & permissions (RBAC)
Spatie roles, the HasRolePermissions gate with its pivot fallback, real permission prefixes, and the roles across tenant, agency and platform panels.
Roles & permissions (RBAC)
Authorization is Spatie permissions on top of the tenant pivot, wrapped by one trait so every Filament resource answers the same way. This page maps the system: the traits, the real permission names, the roles, and the two things that are not authorization.
Who this is for
Developers adding resources, changing role behavior, or debugging "why can this user not see that page?".
Difficulty: Advanced · Time: about 10 minutes.
The roles
| Panel | Roles |
|---|---|
Tenant (/admin) |
admin (owner), manager, member (sales agent) — stored on the tenant_users pivot |
| Platform | super_admin |
Agency (/agency) |
account_manager, ads_specialist, creative_specialist, qa_reviewer (only role that can launch campaigns), support_agent |
The canonical super-admin check
Always use:
$user->isSuperAdmin() // User.php: (bool) $this->is_super_admin || $this->hasRole('super_admin')
The legacy is_super_admin column and the Spatie super_admin role both count. Do not test the column directly.
The resource gate: HasRolePermissions
Every tenant-facing Filament resource declares a prefix and inherits the gates:
use App\Filament\Concerns\HasRolePermissions;
protected static string $permissionPrefix = 'leads';
| Gate | Required permission |
|---|---|
canViewAny |
{prefix}.view |
canView |
{prefix}.view or {prefix}.edit |
canCreate |
{prefix}.create |
canEdit |
{prefix}.edit (view-only roles cannot save) |
canDelete / canDeleteAny |
{prefix}.delete or {prefix}.manage |
userHasAny() resolves in this order:
- Super Admin bypass —
isSuperAdmin()grants everything. - Role-admin bypass — any user holding the Spatie role
admin(the workspace owner;DatabaseSeederwires every tenant permission to this role). - Spatie permission —
hasAnyPermission([...]). tenant_userspivot fallback — for legacy workspaces whose owner never got Spatie roles synced: pivot roleadmin→ everything; pivot rolemanager→ permissions ending.view,.create,.edit,.assign,.export,.import(never.delete/.manage).
Resources with nuanced policies override individual gates — the trait's methods are plain statics, not final.
Real prefixes in use
Prefix values found on tenant resources (a representative inventory):
| Prefix | Example uses | Generated permissions |
|---|---|---|
leads |
LeadResource, LeadCallResource, tags, custom fields, scoring rules, invoices | leads.view/.create/.edit/.delete |
forms |
FormResource, LandingPageResource, LeadCaptureWidgetResource, ChatbotConfigResource | forms.* |
pipeline |
PipelineResource | pipeline.* |
automations |
AutomationResource, EmailSequenceResource, EmailTemplateResource, AiSdrAgentResource | automations.* |
integrations |
IntegrationResource, LeadSourceConnectionResource, OutboundWebhookResource | integrations.* |
api_keys |
ApiKeyResource | api_keys.* |
Prefixes are more coarse-grained than the resource names Several resources deliberately share one prefix (for example everything lead-adjacent uses leads, so a lead role manages calls and tags too). When adding a resource, reuse the prefix family it belongs to rather than inventing a new one unless the role model really differs.
What is NOT authorization
App\Support\NavTier(NavTier::managerOrAbove()) controls navigation visibility only — whether an item is shown in the sidebar. It never decides access; the resource gates above do. Never rely on a hidden nav item for security.- The
member/manager/adminpivot label alone does not confer Spatie permissions except through the fallback above; the Spatie permission set is the primary mechanism.
Agency permissions
The agency panel uses its own meta.* permission set defined in AgencyRolesSeeder::ROLE_PERMISSIONS — for example campaign and creative management actions, with campaign launch reserved to qa_reviewer. Agency users do not hold tenant permissions; the panels are separate.
Where to look in code
| Concern | File |
|---|---|
| Resource gate + fallback | app/Filament/Concerns/HasRolePermissions.php |
| Canonical SA check | app/Models/User.php (isSuperAdmin()) |
| Nav visibility helper | app/Support/NavTier.php |
| Agency permission map | database/seeders/AgencyRolesSeeder.php |
| Route-level middleware aliases | bootstrap/app.php (role, permission, role_or_permission) |
Route-level checks use the aliases above, e.g. ->middleware('permission:api_keys.manage'), for the few non-resource routes that need them.
Related guides
Was this page helpful?
Thanks — noted. ✦