Skip to content

Permissions & Approval

Capell Permissions & Approval screenshot Capell Permissions & Approval screenshot

Capell Admin ships with site-scoped RBAC on top of Spatie’s laravel-permission package, plus page-type role restrictions. The workspace approval workflow is supplied by the optional capell-app/publishing-studio package and documented in PublishingStudio & Versions.

For an end-to-end walkthrough with a flow diagram and the email notifications sent at each transition, see Page creation and approval flow in the Publishing Studio docs.


Spatie’s team feature is enabled with team_foreign_key = site_id. Every role and permission assignment is scoped to a single site, so an editor for Site A does not implicitly have the same role on Site B.

  • MiddlewareSetSitePermissionScope sets the active Spatie team to the current request’s site_id before any authorization check runs. Registered on the admin route group.
  • Trait — user models use HasSitePermissions, which wraps Spatie’s role/permission helpers with site-aware variants (e.g. assignRoleOnSite, hasRoleOnSite).
  • Admin — the Site edit page has a Permissions relation manager where you add users, assign roles for this site, and grant per-user overrides.

The page_role_restrictions table uses a polymorphic restrictable morph so restrictions are defined once per page type rather than per page. On the page type configurator admin tab a multi-select syncs the allowed roles for that page type. Users without any of the listed roles cannot see or edit pages of that type, enforced through PagePolicy.

Model: Capell\Core\Models\PageRoleRestriction.

When capell-app/publishing-studio is installed, approvals are tracked at the workspace level, not per page. A workspace collects all edits that should ship together and moves through the state machine open -> in_review -> approved -> publishing -> published. Capell\PublishingStudio\Models\WorkspaceApproval is the immutable audit row for every submit, approve, reject, or request-changes event.

See PublishingStudio & Versions — Approval lifecycle for the full machine and the Filament actions that drive it.

Gate-based access control throughout the admin:

PolicyModel (Shield subject)Covers
PagePolicyPageview/create/update/delete/publish + page-type role restriction check
SitePolicySiteview/manage sites and the Permissions relation manager
LayoutPolicyLayoutlayout access
MediaPolicyMediamedia access
RedirectPolicyPageUrlredirect CRUD + import/export gates (redirects are stored in PageUrl)
UserPolicyuser modeluser access and user management

Navigation policies are package-owned by capell-app/navigation. Workspace policies and approval gates are package-owned by capell-app/publishing-studio.

All Filament resources resolve their actions through these policies — there are no inline Gate::allows() checks in Blade or resource classes.

Policy registration (do NOT rely on Filament-only discovery)

Section titled “Policy registration (do NOT rely on Filament-only discovery)”

Policies must be globally registered via Gate::policy() in AdminServiceProvider::registerPolicies(). Capell’s policies live in Capell\Admin\Policies\* and gate models in Capell\Core\Models\*; Laravel’s convention-based resolver looks for App\Policies\{Model}Policy and will not find them. Filament’s resource system wires them up for requests going through a panel — but anything invoked outside that context (Actions dispatched from CLI, bulk actions, queued jobs, tests) goes through the raw Gate facade, which returns “denied by default” for every ability when the policy isn’t globally registered.

The bulk-move-pages bug (April 2026) was this exact mistake: only WorkspacePolicy was registered, so every other policy silently returned denied for bulk actions. PolicyRegistrationTest (tests/Admin/Feature/Policies/PolicyRegistrationTest.php) locks the registration contract.

Permission naming — Shield configuration matters

Section titled “Permission naming — Shield configuration matters”

Spatie permission names are generated by Filament Shield using the host app’s config/filament-shield.php:

  • permissions.case (default pascal) — controls the casing of the affix and subject.
  • permissions.separator (default :) — the joiner between affix and subject.

With the defaults, the permission for PagePolicy::update() is Update:Page. Legacy Shield setups used lower_snake + _, producing update_page. Policies must not hardcode either format$user->hasPermissionTo('update_page') throws PermissionDoesNotExist if the host uses the other convention, and the Gate layer swallows the exception as “denied”, hiding the real cause.

Use the ResolvesShieldPermission trait (packages/admin/src/Policies/Concerns/ResolvesShieldPermission.php) in every policy:

use Capell\Admin\Policies\Concerns\ResolvesShieldPermission;
class PagePolicy
{
use ResolvesShieldPermission;
private const SUBJECT = 'Page';
public function update(User $user, Page $page): bool
{
return $user->hasPermissionTo(self::permission('update', self::SUBJECT))
&& $page->isAccessibleByUser($user);
}
}

Custom abilities (e.g. manage_restrictions on PagePolicy, import / export on RedirectPolicy, update_own / manage_permissions on SitePolicy) produce permission names like ManageRestrictions:Page — these are not auto-generated by Shield’s resource scan. Host apps must register them in filament-shield.php → custom_permissions.

presentation.manage_advanced gates advanced presentation and delivery controls. Users without it can still use the basic editor-facing presentation and interaction controls, but they do not see lower-level delivery settings such as lazy fragment delivery, loading strategy, connection requirement, viewport range, or custom width.

The install and upgrade permission sync grants this permission to super_admin by default. Assign it deliberately to editors who understand delivery/performance tradeoffs.

ConcernFile
Middlewarepackages/admin/src/Http/Middleware/SetSitePermissionScope.php
User traitpackages/core/src/Models/Concerns/HasSitePermissions.php
Page-type restrictionpackages/core/src/Models/PageRoleRestriction.php
Host Admin policiespackages/admin/src/Policies/*Policy.php
Extension approval modelsOwning package docs and source
Extension approval actionsOwning package docs and source