When building complex web applications, managing who can access what is paramount. While popular packages like spatie/laravel-permission are fantastic, they can sometimes be overkill or restrictive if you need a highly bespoke solution.
In this guide, we'll architect and build a robust, native Role and Permission authorization system in Laravel from the ground up, utilizing Laravel's built-in Gates and Policies.
Step 1: The Database Structure
We need three main tables (aside from users): roles, permissions, and a pivot table role_permission. A user will belong to a role, and a role will have many permissions.
Generate the migrations:
php artisan make:migration create_roles_table
php artisan make:migration create_permissions_table
php artisan make:migration create_role_permission_table
The schema logic is simple. users gets a role_id foreign key. The pivot table ties role_id and permission_id.
Step 2: Defining the Models
Next, define the Eloquent relationships. A User belongsTo a Role. A Role belongsToMany Permissions.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
public function permissions()
{
return $this->belongsToMany(Permission::class);
}
}
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
public function role()
{
return $this->belongsTo(Role::class);
}
public function hasPermission($permissionSlug)
{
return $this->role->permissions->contains('slug', $permissionSlug);
}
}
The hasPermission helper method on the User model checks if their assigned role contains the given permission slug (e.g., edit_posts).
Step 3: Registering Laravel Gates
Laravel's authorization system resolves around Gates. Instead of manually checking auth()->user()->hasPermission() everywhere, we can hook into the Gate facade to define abilities globally.
Open app/Providers/AuthServiceProvider.php and update the boot method:
use Illuminate\Support\Facades\Gate;
use App\Models\Permission;
public function boot()
{
$this->registerPolicies();
try {
// Fetch all permissions from DB
Permission::get()->map(function ($permission) {
Gate::define($permission->slug, function ($user) use ($permission) {
return $user->hasPermission($permission->slug);
});
});
} catch (\Exception $e) {
// Handle scenario where DB is not migrated yet
}
}
This dynamic registration creates a Gate for every permission in your database.
Step 4: Protecting Routes and Views
Now that Gates are registered, you can use Laravel's standard authorization features everywhere.
In Routes:
Route::post('/posts', [PostController::class, 'store'])
->middleware('can:create_posts');
In Controllers:
public function destroy(Post $post)
{
$this->authorize('delete_posts');
$post->delete();
}
In Blade Views:
@can('edit_posts')
<a href="/posts/edit">Edit Post</a>
@endcan
Conclusion
Building a custom role and permission system in Laravel is surprisingly straightforward thanks to Eloquent and Gates. By maintaining control over the implementation, you can easily adapt it for multi-tenancy, granular team permissions, or any other complex authorization requirement your application may face.