Laravel 12 Roles and Permissions (Without Any Packages) – A Complete Guide

Author

Kritim Yantra

Mar 27, 2025

Laravel 12 Roles and Permissions (Without Any Packages) – A Complete Guide

Managing user roles and permissions is essential for controlling access in web applications. While packages like Spatie Laravel-Permission exist, you can easily implement this in pure Laravel without external dependencies.

In this guide, we’ll cover:
Database Setup for Roles & Permissions
Assigning Roles to Users
Checking Permissions in Controllers & Views
Middleware for Role-Based Access

Let’s build a role-based system from scratch!


1. Why Build Roles & Permissions Without a Package?

🔹 Full control over the logic
🔹 No dependency on third-party packages
🔹 Lightweight & customizable
🔹 Great for learning Laravel’s core features


2. Database Structure

We’ll use 4 tables:

  1. users (Default Laravel users table)
  2. roles (Admin, Editor, User)
  3. permissions (create-post, edit-post, delete-post)
  4. permission_role (Pivot table linking roles and permissions)

Step 1: Create Migrations

Run these commands:

php artisan make:migration create_roles_table
php artisan make:migration create_permissions_table
php artisan make:migration create_permission_role_table

a) roles Table Migration

Schema::create('roles', function (Blueprint $table) {
    $table->id();
    $table->string('name'); // admin, editor, user
    $table->timestamps();
});

b) permissions Table Migration

Schema::create('permissions', function (Blueprint $table) {
    $table->id();
    $table->string('name'); // create-post, edit-post, delete-post
    $table->timestamps();
});

c) permission_role Pivot Table

Schema::create('permission_role', function (Blueprint $table) {
    $table->foreignId('role_id')->constrained();
    $table->foreignId('permission_id')->constrained();
    $table->primary(['role_id', 'permission_id']);
});

d) Add role_id to users Table

php artisan make:migration add_role_id_to_users_table
Schema::table('users', function (Blueprint $table) {
    $table->foreignId('role_id')->default(3)->constrained(); // Default role: 'user'
});

Run migrations:

php artisan migrate

3. Setting Up Models & Relationships

a) User Model

// app/Models/User.php
public function role()
{
    return $this->belongsTo(Role::class);
}

public function hasPermission($permissionName)
{
    return $this->role->permissions()->where('name', $permissionName)->exists();
}

b) Role Model

// app/Models/Role.php
public function permissions()
{
    return $this->belongsToMany(Permission::class);
}

c) Permission Model

// app/Models/Permission.php
public function roles()
{
    return $this->belongsToMany(Role::class);
}

4. Seeding Roles & Permissions

Let’s populate the database with sample data.

Create a Seeder

php artisan make:seeder RolePermissionSeeder
// database/seeders/RolePermissionSeeder.php
public function run()
{
    // Create Roles
    $admin = Role::create(['name' => 'admin']);
    $editor = Role::create(['name' => 'editor']);
    $user = Role::create(['name' => 'user']);

    // Create Permissions
    $createPost = Permission::create(['name' => 'create-post']);
    $editPost = Permission::create(['name' => 'edit-post']);
    $deletePost = Permission::create(['name' => 'delete-post']);

    // Assign Permissions to Roles
    $admin->permissions()->attach([$createPost->id, $editPost->id, $deletePost->id]);
    $editor->permissions()->attach([$createPost->id, $editPost->id]);
    $user->permissions()->attach([$createPost->id]);
}

Run the seeder:

php artisan db:seed --class=RolePermissionSeeder

5. Checking Permissions in Controllers

Example: Restrict Post Creation to Users with Permission

// app/Http/Controllers/PostController.php
public function create()
{
    if (!auth()->user()->hasPermission('create-post')) {
        abort(403, 'Unauthorized action.');
    }
    return view('posts.create');
}

6. Middleware for Role-Based Access

Step 1: Create Middleware

php artisan make:middleware CheckRole

Step 2: Define Logic

// app/Http/Middleware/CheckRole.php
public function handle($request, Closure $next, $role)
{
    if (auth()->check() && auth()->user()->role->name === $role) {
        return $next($request);
    }
    abort(403, 'Access Denied');
}

Step 3: Register Middleware

Add this tobootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
        $middleware->alias([
            'role' => \App\Http\Middleware\CheckRole::class,
        ]);
    })

Step 4: Protect Routes

Route::get('/admin/dashboard', function () {
    return view('admin.dashboard');
})->middleware('role:admin');

7. Checking Permissions in Blade Views

@if(auth()->user()->hasPermission('edit-post'))
    <a href="/posts/{{ $post->id }}/edit">Edit Post</a>
@endif

8. Assigning Roles to Users

// Make a user an admin
$user = User::find(1);
$user->role_id = 1; // admin role
$user->save();

9. Best Practices

Use caching for permissions to reduce database queries.
Avoid hardcoding role/permission names (use constants).
Test permissions thoroughly with unit tests.


Conclusion

You’ve just built a full role-permission system in Laravel without any packages!

🔹 Database Setup → Roles, Permissions, Pivot Tables
🔹 Models & RelationshipsUser, Role, Permission
🔹 Middleware & Controllers → Restrict access
🔹 Blade Checks → Show/hide UI elements

🚀 Now go ahead and implement this in your Laravel app!

Tags

Comments

Сергей Галбур

Сергей Галбур

Oct 16, 2025 02:08 PM

from https://laravel.com/docs/12.x/eloquent-relationships#many-to-many-model-structure To determine the table name of the relationship's intermediate table, Eloquent will join the two related model names in alphabetical order. However, you are free to override this convention. You may do so by passing a second argument to the belongsToMany method:To determine the table name of the relationship's intermediate table, Eloquent will join the two related model names in alphabetical order. However, you are free to override this convention. You may do so by passing a second argument to the belongsToMany method. I did: public function roles() { return $this->belongsToMany(Role::class, 'role_permission'); } Thanks for the article and the answer to my question.
K

Kritim Yantra

Oct 20, 2025 02:26 PM

cool😊👍
Сергей Галбур

Сергей Галбур

Oct 12, 2025 08:03 AM

Hi. It's a very good article. I'm trying to reproduce the above code. But for some reason, when launching the seeder after the php artisan db:seed --class=RolePermissionSeeder command, the seeder tries to fill in the permission_role table instead of role_permission. This causes the error "permission_role'doesn't exist'"
K

Kritim Yantra

Oct 12, 2025 02:08 PM

you could rename your migration table to permission_role instead: Schema::create('permission_role', function (Blueprint $table) { $table->foreignId('permission_id')->constrained(); $table->foreignId('role_id')->constrained(); $table->primary(['permission_id', 'role_id']); });

Please log in to post a comment:

Sign in with Google

Related Posts