Authentication and Role Permissions in Laravel with Spatie

Business apps rarely have one type of user. A cashier should only record sales, a manager should see reports, and an admin should be able to delete records. If those differences ar...

Authentication and Role Permissions in Laravel with Spatie

Business apps rarely have one type of user. A cashier should only record sales, a manager should see reports, and an admin should be able to delete records. If those differences are written as if ($user->role == 'admin') all over the codebase, every rule change means hunting through dozens of files, and something always gets missed.

This tutorial builds a clean access system in Laravel 11: authentication with Laravel Breeze, then roles and permissions with spatie/laravel-permission (v6). We start with access design, then the code, then the errors that tend to surface in production.

1. Authentication vs Authorization

  • Authentication answers "who are you?": login, logout, password reset, email verification.
  • Authorization answers "what may you do?": roles, permissions and policies.

Spatie handles authorization on top of Laravel's built-in Gate, so $user->can() and @can keep working as usual.

2. Authentication with Breeze

composer require laravel/breeze --dev
php artisan breeze:install blade
php artisan migrate
npm install && npm run build

You now have login, registration, password reset and profile pages, plus the auth middleware. For internal tools such as a POS or HR system, consider disabling public registration and creating accounts from an admin screen only.

3. Design Access Before Writing Code

Write down the actions, then decide which role may perform each one. An example for a small point-of-sale app:

PermissionCashierManagerAdmin
sales.createYesYesYes
sales.voidNoYesYes
reports.viewNoYesYes
products.manageNoYesYes
users.manageNoNoYes

An object.action naming pattern keeps the list readable and sortable. Whatever you choose, be consistent.

4. Installing spatie/laravel-permission

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

This creates five tables: roles, permissions, model_has_roles, model_has_permissions and role_has_permissions. Add the trait to your User model:

use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
}

5. A Seeder as the Single Source of Truth

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

class RolePermissionSeeder extends Seeder
{
    public function run(): void
    {
        app(PermissionRegistrar::class)->forgetCachedPermissions();

        $matrix = [
            'cashier' => ['sales.create'],
            'manager' => ['sales.create', 'sales.void', 'reports.view', 'products.manage'],
        ];

        $all = collect($matrix)->flatten()->push('users.manage')->unique();

        foreach ($all as $name) {
            Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']);
        }

        foreach ($matrix as $role => $perms) {
            Role::firstOrCreate(['name' => $role, 'guard_name' => 'web'])
                ->syncPermissions($perms);
        }

        Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
    }
}

syncPermissions() replaces a role's permissions with exactly the array you pass, so removing a permission from the matrix and re-running the seeder revokes it. The admin role gets nothing here on purpose; a Gate rule covers it.

6. Super-Admin via Gate::before

use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::before(fn ($user, $ability) => $user->hasRole('admin') ? true : null);
}

Return null for everyone else, not false. Returning false would deny every check for non-admins.

7. Protecting Routes

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'role'               => \Spatie\Permission\Middleware\RoleMiddleware::class,
        'permission'         => \Spatie\Permission\Middleware\PermissionMiddleware::class,
        'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
    ]);
})
Route::middleware('auth')->group(function () {
    Route::post('/sales', [SaleController::class, 'store'])
        ->middleware('permission:sales.create');

    Route::get('/reports', [ReportController::class, 'index'])
        ->middleware('permission:reports.view');
});

The role: middleware compares role names directly and ignores Gate::before, while permission: goes through the Gate, so admins still pass. One more reason to check permissions rather than roles.

8. Checks in Controllers and Blade

use Illuminate\Support\Facades\Gate;

public function destroy(Sale $sale)
{
    Gate::authorize('sales.void');

    $sale->update(['status' => 'void']);

    return back();
}

The Laravel 11 base controller no longer includes AuthorizesRequests, so $this->authorize() fails unless you add the trait back. Gate::authorize() works everywhere.

@can('reports.view')
    <a href="{{ route('reports.index') }}">Reports</a>
@endcan

Hiding a button is cosmetic. Real protection belongs on the route or in the controller, because anyone can type a URL.

9. Permissions Plus Policies

A permission says whether someone may void sales at all. It cannot say whether they may void a sale from another branch. Combine it with a Policy:

public function delete(User $user, Sale $sale): bool
{
    return $user->can('sales.void') && $sale->branch_id === $user->branch_id;
}

10. Common Errors

  • "There is no permission named X for guard web": a typo, or the seeder has not run.
  • Changes do not take effect: cached permissions. Run php artisan permission:cache-reset.
  • Unknown middleware: alias missing, or an old namespace (Middlewares in v5, Middleware in v6).
  • API requests always get 403: permissions exist for the web guard but the request uses another guard.

Implementation Checklist

  • A written access matrix approved by the business owner.
  • All roles and permissions created by a seeder.
  • Code checks permissions, not role names.
  • Every sensitive route has middleware, not just a hidden button.
  • A test confirms that a cashier gets 403 on the reports page.

For related hardening, read 10 must-have web security practices, and for per-client roles see building a multi-tenant SaaS with Laravel.

Yudhi
Written by
Yudhi
Founder & Lead Developer, GudangCode

Yudhi is the founder of GudangCode and a Laravel developer who has built dozens of ready-to-use business information systems — from POS and HRIS to management apps. He writes guides and articles on GudangCode to help Indonesian developers run, understand, and deploy Laravel source code correctly.

LaravelPHPMySQLSistem Informasi Bisnis See all articles by Yudhi
Want the full source code & apps?

Sign up free to download ready-to-use business applications, information systems, and Laravel source code.

Sign Up Free & Download
Laravel Spatie Role Permission Keamanan
Share this article
Back to Blog
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →

Get Full Access Now!

Join our membership and unlock exclusive access to all premium features. Fast, easy, and ready to use instantly.

Join Membership Now