Reader Stacks

Roles and Permissions in Laravel With spatie/laravel-permission

The most widely used role/permission package for Laravel — assigning roles, checking permissions in code and Blade, and the middleware that gates routes by role.

spatie/laravel-permission is the most widely adopted role and permission package for Laravel — it adds roles and granular permissions on top of Laravel's own auth system rather than replacing it.

1. Install and set up

composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate

Add the trait to your User model:

use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
}

2. Create roles and permissions

use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;

$editorRole = Role::create(['name' => 'editor']);
$permission = Permission::create(['name' => 'edit articles']);
$editorRole->givePermissionTo($permission);

3. Assign roles to users

$user->assignRole('editor');
$user->givePermissionTo('publish articles'); // direct permission, no role needed

Permissions can be assigned directly to a user, or granted implicitly through a role — a user has a permission if it's attached directly OR through any role they hold.

4. Checking permissions in code

if ($user->can('edit articles')) {
    // ...
}

if ($user->hasRole('editor')) {
    // ...
}

5. In Blade templates

@can('edit articles')
    <a href="https://readerstacks.com/articles/{{ $article->id }}/edit">Edit</a>
@endcan

6. Gating routes with middleware

Route::middleware(['role:editor'])->group(function () {
    Route::get('/admin/articles', [ArticleController::class, 'index']);
});

Route::middleware(['permission:publish articles'])->group(function () {
    Route::post('/articles/{article}/publish', [ArticleController::class, 'publish']);
});

Roles vs. direct permissions — when to use which

Roles are the right tool when a group of permissions genuinely travels together (an "editor" always gets the same bundle of abilities). Direct permissions are better for one-off exceptions — granting a single extra permission to one specific user without creating a whole new role just for them.

Register the package middleware aliases on modern Laravel

The route examples use role: and permission: aliases. In a current Laravel application, register those package middleware classes in bootstrap/app.php if they are not already configured:

use Illuminate\Foundation\Configuration\Middleware;
use Spatie\Permission\Middleware\PermissionMiddleware;
use Spatie\Permission\Middleware\RoleMiddleware;
use Spatie\Permission\Middleware\RoleOrPermissionMiddleware;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->alias([
        'role' => RoleMiddleware::class,
        'permission' => PermissionMiddleware::class,
        'role_or_permission' => RoleOrPermissionMiddleware::class,
    ]);
})

If a route reports that role is not a middleware class, the problem may be alias registration rather than the user's assigned roles.

Prefer permission checks over role-name checks for application behavior

A role is a convenient bundle of abilities; a permission is usually the actual business rule. Code that asks $user->can('edit articles') survives a future reorganization where editors and moderators both gain that ability. Code that checks hasRole('editor') everywhere hard-wires the application to today's role taxonomy.

Guard names are part of the identity of roles and permissions

Applications using more than one auth guard can have roles and permissions scoped by guard. A role created for one guard is not automatically interchangeable with the same text name under another guard. If assignment unexpectedly throws a guard mismatch, inspect the model's guard and the role/permission records before recreating data.

Cached permissions can make manual database edits misleading

The package caches permission relationships for performance. Use the package's APIs for role and permission changes rather than editing pivot tables by hand; those APIs know when cached permission state needs to be invalidated. If you are debugging a migration or seeder that bypassed normal APIs, clear the package permission cache before concluding the authorization code is wrong.

Topics: Authentication & Access Control