The simplest way to add roles and permissions to Laravel 13 is the spatie/laravel-permission package. Install it, publish and run its migrations, add the HasRoles trait to your User model, create roles and permissions in a seeder, then protect routes with the role and permission middleware. This guide walks through each step with code.
Laravel itself ships gates and policies for authorization. A roles-and-permissions package adds the database tables and helper methods that let you assign roles to users and check them anywhere in your app.
What do I need before I start?
- Laravel 13.x on PHP 8.3 or newer, with authentication already working.
- A database connection and a
userstable. - Check the package's prerequisites page for the Laravel and PHP versions supported by its current major release before you install.
Step 1: How do I install the package?
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan optimize:clear
php artisan migrateThe service provider registers itself automatically. The publish command copies the config file and the migration that creates the roles, permissions and pivot tables.
Step 2: How do I add roles to the User model?
use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
// ...
}Step 3: How do I create roles and permissions?
Use a seeder so the setup is repeatable. Name permissions after the action they allow, for example posts.create. Assign permissions to roles, and roles to users.
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;
class RolesSeeder extends Seeder
{
public function run(): void
{
app()[PermissionRegistrar::class]->forgetCachedPermissions();
foreach (['posts.view', 'posts.create', 'posts.delete'] as $name) {
Permission::findOrCreate($name, 'web');
}
Role::findOrCreate('editor', 'web')->syncPermissions(['posts.view', 'posts.create']);
Role::findOrCreate('admin', 'web')->syncPermissions(Permission::all());
User::firstWhere('email', 'admin@example.com')?->assignRole('admin');
}
}I ran php -l on this seeder to check the syntax. I did not run it against a database for this article. Run it with php artisan db:seed --class=RolesSeeder.
The first line clears the package's permission cache. Without it, a seeder that runs twice can read stale data.
Step 4: How do I protect routes?
In Laravel 11 and later, register the middleware aliases in bootstrap/app.php:
use Illuminate\Foundation\Configuration\Middleware;
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
]);
})Then use them on routes:
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get('/admin', [AdminController::class, 'index']);
});
Route::post('/posts', [PostController::class, 'store'])
->middleware(['auth', 'permission:posts.create']);How do I check roles and permissions in code and views?
$user->hasRole('admin');
$user->hasAnyRole(['admin', 'editor']);
$user->can('posts.delete');
$user->givePermissionTo('posts.view');
$user->removeRole('editor');Because the package integrates with Laravel's gate, the built-in @can directive works in Blade, and the package adds @role too:
@can('posts.delete')
<button>Delete</button>
@endcan
@role('admin')
<a href="/admin">Admin area</a>
@endroleShould I check roles or permissions?
Check permissions in your code and use roles only as named bundles of permissions. A check like can('posts.delete') keeps working if you later add a "moderator" role. A check like hasRole('admin') breaks as soon as another role needs the same access.
How do I verify it works?
- Seed the roles and assign
editorto a test user. - Log in as that user and open an admin route. You should get a 403 response.
- Assign
adminand reload. The route should now load. - Write a feature test that acts as each user and asserts the status code, so the rules cannot regress silently.
it('blocks editors from the admin area', function () {
$user = User::factory()->create()->assignRole('editor');
$this->actingAs($user)->get('/admin')->assertForbidden();
});This test uses Pest syntax and is shown as a pattern. It needs the roles to exist in the test database, so call the seeder in your test setup. I have not executed it here.
What are the common mistakes?
- Stale cache. Permissions are cached. After changing them by hand in the database, run
php artisan permission:cache-reset. - Wrong guard. A role created for one guard does not apply to a user authenticated through another guard. Keep the guard name consistent.
- Forgetting the trait. Without
HasRoles, methods likeassignRoledo not exist. - Hiding buttons only. Hiding a button in Blade is not security. Always protect the route too.
- Skipping migrate. The tables must exist before you seed.
FAQ
Do I need a package for roles in Laravel?
No. Gates and policies cover many cases. A package becomes useful when admins must create roles and assign them at runtime, because that data needs database tables.
Can a user have more than one role?
Yes. A user can hold several roles, and permissions from all of them apply. Use assignRole repeatedly or pass an array.
Can I give a single permission directly to a user?
Yes, with givePermissionTo. Use this sparingly, since role-based assignment is easier to audit.



