Skip to content

Roles ​

Every panel user has exactly one role. The ladder from top to bottom:

RoleSees and edits
developereverything an admin can, plus the technical settings (environment type, debug, banner, mail safety, preview protection, module switches) and the devtools (env editor, routes, blueprints, logs, going live checklist)
adminthe whole panel: content, leads, users, settings (general, mail, tracking, video, packages, health), activity, health monitoring
content_managerpages, navigation, media, forms, sections, tags; the cache screen
crm_managerleads, on top of what a content manager sees for the content screens they are allowed into
event_managerevents and participants

Only a developer can hand out the developer role. The role select in the user and invite forms hides it from admins, and the server refuses the assignment should the form be tampered with.

Who is a developer ​

New installs seed the first user as developer. Existing installs get the role with the 2.7.0 migration, which hands it to every login on the racerfish.com domain; anyone else is promoted from the panel by a developer.

The intent is a plain one: the person who deploys and configures a site is the developer, the client's webmaster is the admin. An admin cannot accidentally switch a staging site to live, turn off mail safety, or disable a module; a developer can, and the activity log records who did.

Restricting panel areas 2.12 ​

Which roles reach the media library, the AI editor and each module is configured under evolve.roles in config/evolve.php. The defaults keep the behaviour of earlier releases.

php
'roles' => [
    'media' => ['developer', 'admin', 'content_manager'],
    'editor-ai' => ['developer', 'admin', 'content_manager'],
    'modules' => [
        // '*' => ['developer', 'admin', 'content_manager'],
        // 'blog' => ['developer', 'admin', 'content_manager', 'editor'],
    ],
],
KeyDefaultControls
roles.mediadeveloper, admin, content_managerthe media library routes and its sidebar entry
roles.editor-aideveloper, admin, content_managerthe AI editor routes
roles.modules.<name>noneevery panel route a module registers with Route::evolveModule(), and its sidebar entry
roles.modules.*nonefallback for every module without its own entry

How module access resolves: the module's own entry wins, otherwise *, otherwise the module is open to every logged-in panel user (the behaviour before 2.12). A user without one of the listed roles gets a 403 on the module's routes, and the sidebar hides the module.

There is no env variable for these lists; they are project code and belong in the published config/evolve.php.

Example: an editorial role for the blog ​

A club site where volunteers write blog posts and upload their photos, but must not touch pages, other modules or settings. The role is called redaktionsmitglied.

  1. Create the role in a project migration:

    php
    use Illuminate\Database\Migrations\Migration;
    use Spatie\Permission\Models\Role;
    
    return new class extends Migration
    {
        public function up(): void
        {
            Role::findOrCreate('redaktionsmitglied', 'web');
        }
    
        public function down(): void
        {
            Role::query()->where('name', 'redaktionsmitglied')->where('guard_name', 'web')->delete();
        }
    };
  2. Give it the blog and the media library, and close every other module to it:

    php
    'roles' => [
        'media' => ['developer', 'admin', 'content_manager', 'redaktionsmitglied'],
        'editor-ai' => ['developer', 'admin', 'content_manager', 'redaktionsmitglied'],
        'modules' => [
            '*' => ['developer', 'admin', 'content_manager'],
            'blog' => ['developer', 'admin', 'content_manager', 'redaktionsmitglied'],
        ],
    ],
  3. Assign the role to the volunteers in the user screen. Every role in the database is assignable there (the developer role only by developers).

The * entry is what closes the other modules. Without it, every module without an entry stays open to every logged-in user, including the new role.

Always list developer and admin

The lists are checked literally with the role middleware. A role that is not listed does not get in, and that includes developer: there is no automatic bypass for route access in 2.12. A * entry without developer locks the developers out of every module it covers. Copy the defaults and add to them.

Core screens that are not modules (pages, navigation, forms, settings, users) keep their built-in role checks and are not configurable here.

Verifying ​

Log in as a user with the custom role (or have an admin assign it to a test account): the sidebar shows only the permitted modules, and opening another module's URL directly, for example /evolve/event, answers 403. On the server, php artisan route:list --path=evolve/blog -v shows the role:... middleware each module route carries.

Permissions 2.12 ​

2.12 adds a catalogue of permissions (spatie/laravel-permission, guard web) and grants it to the built-in roles, so that access can later be granted per ability instead of per role name. In 2.12 nothing is decided by these permissions yet. Routes, views and evolve.roles still check role names. A later release switches the checks to permissions (can: middleware and policies), shared by the panel, the MCP tools and the API.

The catalogue:

PermissionGranted to by default
pages.edit, pages.create, pages.delete, pages.publishdeveloper, admin, content_manager
pages.set-homedeveloper, admin
navigation.editdeveloper, admin, content_manager
navigation.managedeveloper, admin
sections.editdeveloper, admin, content_manager
sections.managedeveloper, admin
forms.manage, static-strings.manage, redirects.manage, languages.manage, regions.managedeveloper, admin, content_manager
regions.switchdeveloper, admin
cache.manage, panel.searchdeveloper, admin, content_manager
leads.viewdeveloper, admin, content_manager, crm_manager
users.manage, tags.manage, activity.view, health.view, settings.manage, system.info, system.updatedeveloper, admin
media.managethe roles in evolve.roles.media
media.submit, media.approve, media.edit-rights, media.view-restricteddeveloper, admin, media.submit also to every role with media.manage; only with the Media Governance module (2.22)
editor-ai.usethe roles in evolve.roles.editor-ai
modules.<name>.accessthe roles evolve.roles.modules resolves for the module, or all built-in roles when it has no entry
settings.technical, devtools.access, roles.assign-developerdeveloper only

How the grants are applied:

  • The migration 2026_09_26_000000_add_panel_permissions creates missing permissions and grants them to the roles named above that exist in the database. Fresh installs get the same grants from the roles seeder.
  • It only adds, never revokes, and leaves manual grants alone. Running it twice changes nothing.
  • The media, AI and module grants are read from evolve.roles at the time the migration runs. A project with a customised evolve.roles should have that config deployed before or together with the migration. A custom role listed there (like redaktionsmitglied above) receives those grants when it exists by then; custom roles get nothing else.
  • A Gate::before hook lets developers pass every Evolve ability and admins every one except the developer-only three. Abilities a project defines itself are left to its own gates and policies.

To check the grants of a role on a server:

bash
php artisan permission:show web