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 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
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.

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 afterCreating hook to ensure every test user has a role, or your authorization tests will fail by default.