Features

Roles & permissions

Roles and permissions are managed in admin UI backed by Spatie Permission. Canonical permission names live in PermissionEnum; permissions:sync (and the UI sync button) upsert them into the database. Seeded roles are super-admin, admin, reader, and the system role public (anonymous Public CMS API actor).

Scale with groups

For large memberships, attach roles to a group instead of assigning roles user-by-user. Members inherit admin Spatie permissions and the role’s collection/file access matrices. There is no separate group→collection ACL.

Never invent permission strings

Only use values from App\Enums\PermissionEnum. Creating ad-hoc names in the DB that are not in the enum will drift from the TypeScript mirror (resources/js/enums/permission-enum.ts) and from permissions:sync --prune. Full resolution rules: Effective permissions.

Permissions (admin CRUD area)

These gate the roles/permissions screens themselves:

AreaExact strings
Rolescan-show-roles, can-create-roles, can-edit-roles, can-delete-roles
Permissionscan-show-permissions, can-create-permissions, can-edit-permissions, can-delete-permissions

Roles

Controller & pages

ItemDetail
ControllerApp\Http\Controllers\Admin\RoleController
HelperApp\Support\Authorization\PermissionGrouper (groups permissions for the form UI)
Pagesresources/js/pages/admin/roles/index.tsx, form.tsx

Role create/edit shows Unsaved + Discard in the form header while dirty (Discard resets the form in place). Delete on the roles index opens a confirm dialog. Bulk deletes elsewhere (users, groups, collections) also confirm with a selected count.

Routes

ActionRoutePermission
IndexGET /settings/rolescan-show-roles
Create formGET /settings/roles/createcan-create-roles
StorePOST /settings/rolescan-create-roles
Edit formGET /settings/roles/{role}/editcan-edit-roles
UpdatePUT /settings/roles/{role}can-edit-roles
DestroyDELETE /settings/roles/{role}can-delete-roles
BulkPOST /settings/roles/bulk-actionsAuthorized in FormRequest

Store/update validate name, optional permission_ids[], optional collection_permissions (per-collection create/read/update/delete), and optional file_permissions (global create/read/read_private/update/delete for the Public CMS API), then sync Spatie permissions / collection / file grants and flush caches.

Bulk: action delete only. BulkRoleActionRequest::deletableIds() skips super-admin.

Super-admin & public protection

super-admin cannot be deleted or renamed (403 on destroy; bulk skips it). public is system / not assignable: cannot rename, delete, or attach to users; Spatie permission checkboxes are hidden — only Collection access and Files access are editable.

Collection access (Public CMS API)

On role create/edit, the Collection access matrix grants headless /api/v1 actions independently of Spatie admin permissions:

UIMeaning
✓ (green)Action allowed for that collection
✕ (red)Action denied (default if never granted)

Actions: Create, Read, Update, Delete. Missing grant = deny.

Use this on:

  • public — anonymous callers (no Bearer token)
  • Custom roles — then bind an API key to that role for partners/backends

Full flow, endpoints, and auth resolution: Public CMS API.

Files access (Public CMS API)

Same role form → Files access matrix. Grants are global (not per folder), stored in file_permissions:

UI actionStored actionMeaning
CreatecreateUpload via POST /api/v1/files
ReadreadList / show / content / transforms for public (effective) files; expand public file fields on items
Read privateread_privateSame endpoints for files whose effective visibility is private
UpdateupdatePatch metadata
DeletedeleteSoft delete

File visibility (files.access): public, private, or null (inherit from nearest ancestor). Effective private without read_private → Files API 403 (or omitted from list); item expanders return null / omit the entry (no id leak). Details: Public CMS API — Public vs private files.

Typical public website pattern

On the public role: grant Read (✓) if the site needs images/files; leave Read private as denied (✕). Use a custom API-key role with Read private only when a backend must fetch private assets.

Permissions (Spatie rows)

Controller & page

ItemDetail
ControllerApp\Http\Controllers\Admin\PermissionController
ModelSpatie\Permission\Models\Permission
Pageresources/js/pages/admin/permissions/index.tsx

Routes

ActionRoutePermission
IndexGET /settings/permissionscan-show-permissions
StorePOST /settings/permissionscan-create-permissions
UpdatePUT /settings/permissions/{permission}can-edit-permissions
DestroyDELETE /settings/permissions/{permission}can-delete-permissions
SyncPOST /settings/permissions/synccan-edit-permissions

The index UI is primarily a read-only list plus Sync from enum (gated by can-edit-permissions). Prefer sync over hand-editing names.

Sync from PermissionEnum

Artisan

php artisan permissions:sync
php artisan permissions:sync --prune
ItemDetail
CommandApp\Console\Commands\SyncPermissionsCommand
Signaturepermissions:sync {--prune}
BehaviorUpserts every PermissionEnum::values() for the default guard
--pruneDeletes DB permissions whose names are not in the enum
Side effectRegenerates resources/js/enums/permission-enum.ts

PermissionSeeder only runs Artisan::call('permissions:sync') (no prune).

UI sync

PermissionController::sync calls the same command with '--prune' => $request->boolean('prune'). The permissions page currently POSTs {}, so prune defaults to false.

Seeded roles

RoleEnum + Database\Seeders\RoleSeeder (after PermissionSeeder):

Role valueSeeded permissions
super-adminAll permissions in the DB for the guard
adminAll permissions in the DB for the guard
readerOnly enum values whose name starts with can-show-
publicNone — system role for anonymous /api/v1 (Collection access + Files access only)

Reader show set (exact):

can-show-dashboard
can-show-users
can-show-groups
can-show-roles
can-show-permissions
can-show-files
can-show-collections
can-show-activity-logs

can-use-ai is not in the reader set. Runtime difference between super-admin and admin: super-admin also bypasses Gates via Gate::before / EffectivePermissionResolver.

DatabaseSeeder order: PermissionSeederRoleSeederCreateSuperAdminSeeder.

Full PermissionEnum catalog

Do not invent strings outside this list:

can-show-dashboard

can-show-users
can-create-users
can-edit-users
can-delete-users
can-restore-users
can-force-delete-users

can-show-groups
can-create-groups
can-edit-groups
can-delete-groups
can-restore-groups
can-force-delete-groups

can-show-roles
can-create-roles
can-edit-roles
can-delete-roles

can-show-permissions
can-create-permissions
can-edit-permissions
can-delete-permissions

can-show-files
can-create-files
can-edit-files
can-delete-files
can-restore-files
can-force-delete-files
can-download-files
can-favorite-files
can-copy-files
can-replace-files
can-tag-files
can-update-file-metadata

can-show-collections
can-create-collections
can-edit-collections
can-delete-collections
can-restore-collections
can-force-delete-collections

can-show-activity-logs
can-use-ai

can-show-api-keys
can-manage-api-keys

can-show-jobs
can-manage-jobs

can-manage-project-settings

Source: app/Enums/PermissionEnum.php. Helper: PermissionEnum::values().

API Keys (admin)

ActionRoutePermission
Index / create UIGET /settings/api-keyscan-show-api-keys
StorePOST /settings/api-keyscan-manage-api-keys
Revoke / destroyDELETE /settings/api-keys/{apiKey}can-manage-api-keys

Keys authenticate the Public CMS API (Authorization: Bearer ek_…). Permissions for API calls come from the key’s role Collection access and Files access matrices — not from Spatie permissions on the creating user. Details: Public CMS API.

Source map

ConcernLocation
Role controllerapp/Http/Controllers/Admin/RoleController.php
Permission controllerapp/Http/Controllers/Admin/PermissionController.php
API keysapp/Http/Controllers/Admin/ApiKeyController.php
Collection permission syncapp/Services/Api/CollectionPermissionSync.php
File permission syncapp/Services/Api/FilePermissionSync.php
Sync commandapp/Console/Commands/SyncPermissionsCommand.php
Enumsapp/Enums/PermissionEnum.php, RoleEnum.php, FilePermissionAction.php, FileAccess.php
Seedersdatabase/seeders/PermissionSeeder.php, RoleSeeder.php

Collection access rules

Beyond the CRUD matrix, each collection grant may include rules.fields and rules.item_filter. Configure under Settings → Roles → Collection access → Fields & item filter.

Full dialect and API effects: Public CMS API — Field-level rules.

These field flags also drive the admin item form UI: no read hides the field; missing create/update (for new vs edit) makes it read-only. Super-admins and Spatie-only admins without a collection grant stay unrestricted in the form (fieldGrants is null). Details: Collection items — Field ACL.

Preview as role

Editors can simulate how a role (including public) sees an item without switching accounts. The item form Preview as… action calls collections.items.preview-as-role and shows the redacted JSON payload (same enforcer + file expander as the public API). See Collection items — Preview as role.

Jobs monitor

PermissionPurpose
can-show-jobsView /settings/jobs (pending sample + failed jobs)
can-manage-jobsRetry / delete failed jobs

Details: Operations — Jobs monitor.

Project settings

PermissionPurpose
can-manage-project-settingsEdit /settings/project, branding /settings/appearance, webhook test

Details: Project settings.

Previous
Users & groups