Zodiac Tech CRM Docs

Roles & permissions (RBAC)

Spatie roles, the HasRolePermissions gate with its pivot fallback, real permission prefixes, and the roles across tenant, agency and platform panels.

developer Advanced ⏱ 10 min Updated 2026-10-05

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:

  1. Super Admin bypass — isSuperAdmin() grants everything.
  2. Role-admin bypass — any user holding the Spatie role admin (the workspace owner; DatabaseSeeder wires every tenant permission to this role).
  3. Spatie permission — hasAnyPermission([...]).
  4. tenant_users pivot fallback — for legacy workspaces whose owner never got Spatie roles synced: pivot role admin → everything; pivot role manager → 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/admin pivot 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.

Was this page helpful?