Refactoring Laravel SAAS: Granular Access with Spatie Permission

Overview

Hardcoding an is_admin boolean into your users table works for simple projects, but it falls apart the moment a client asks for a "Viewer" or "Manager" role. In a SAAS environment, granularity is the goal. By integrating the

package, you transform a rigid binary system into a dynamic database-driven engine. This technique allows you to define specific permissions—like task_create or project_view—and group them into roles that can be assigned and modified without touching a single line of application code.

Prerequisites

To follow this refactor, you should be comfortable with

fundamentals, specifically Eloquent models, migrations, and Form Requests. Familiarity with
PHP Enums
in PHP is beneficial, as they remain the best way to maintain a "source of truth" for role names even when data is stored in the database.

Key Libraries & Tools

  • Spatie Laravel Permission
    : The industry-standard package for managing roles and permissions in the database.
  • Laravel Fortify: Used here for handling user registration hooks.
  • Livewire: Utilized for dynamic interface elements like invitation forms.
Refactoring Laravel SAAS: Granular Access with Spatie Permission
Building Laravel Saas: Part 5/5 - Roles/Permissions

Code Walkthrough

1. Installation and Model Setup

First, pull in the package and apply the HasRoles trait to your User model. This trait provides the necessary relationship methods to link users to their roles and permissions.

use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
    // ...
}

2. Implementation in Policies

Instead of checking roles directly in your controllers, wrap the logic in Laravel Policies. This keeps your authorization logic centralized. Combine the package's hasPermissionTo method with your multi-tenancy ownership checks.

public function update(User $user, Task $task)
{
    return $user->hasPermissionTo('task_edit') 
           && $user->team_id === $task->team_id;
}

3. Database Seeding

Define your roles and permissions in a dedicated seeder. This ensures your environment is consistent across development and production.

$admin = Role::create(['name' => RoleEnum::ADMIN->value]);
$admin->givePermissionTo(Permission::all());

$viewer = Role::create(['name' => RoleEnum::VIEWER->value]);
$viewer->givePermissionTo('task_view');

Syntax Notes

Notice the shift from $user->is_admin to $user->can('task_create'). Laravel’s built-in @can directive in Blade automatically hooks into the Spatie package, allowing you to hide menu items or buttons based on the underlying database permissions seamlessly.

Tips & Gotchas

Always assign a default role during user creation. If you use

for testing, leverage the afterCreating hook to ensure every test user has a role, or your authorization tests will fail by default.

3 min read