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.