Permission systems are one of the most misunderstood parts of backend development. Many developers build systems that work… until they scale — then everything breaks.
If you've ever hardcoded roles like admin = true, this guide is for you.
Common Mistakes Developers Make
Before fixing it, let’s understand the problems:
1. Hardcoding Roles
if ($user->is_admin) {
// allow everything
}
👉 Not scalable. What if you need editor, moderator, manager?
2. Mixing Roles and Permissions
if ($user->role == 'admin' || $user->role == 'editor') {
// allow edit
}
👉 Roles should NOT define logic directly.
3. No Granular Control
- Can’t control create / read / update / delete separately
- Everything becomes “all or nothing”
4. No Future Flexibility
- Adding new features = rewriting authorization logic
The Correct Design (Industry Standard)
Modern systems use 3 layers:
🔹 1. Permissions (Atomic Level)
Smallest unit of access:
-
user.create -
user.read -
user.update -
user.delete
🔹 2. Roles (Group of Permissions)
Roles are just collections of permissions:
- Admin → all permissions
- Editor → create, read, update
- Viewer → read only
🔹 3. Users (Assigned Roles)
Users get access via roles:
User → Role → Permissions
Think Like This
❌ Wrong: "User is admin → allow everything"
✅ Right: "User has permission → allow action"
Step-by-Step Implementation
Step 1: Database Design
Tables
users
id, name, email, password
roles
id, name
permissions
id, name
role_user (Pivot)
user_id, role_id
permission_role (Pivot)
role_id, permission_id
Step 2: Define Permissions
Example:
[
'user.create',
'user.read',
'user.update',
'user.delete'
]
Step 3: Assign Permissions to Roles
Admin:
- user.create
- user.read
- user.update
- user.delete
Editor:
- user.create
- user.read
- user.update
Viewer:
- user.read
Step 4: Assign Roles to Users
$user->roles()->attach($roleId);
Step 5: Check Permissions (Important!)
❌ Wrong Way
if ($user->role == 'admin')
✅ Correct Way
if ($user->hasPermission('user.update')) {
// allow
}
Step 6: Create Helper Function
public function hasPermission($permission)
{
return $this->roles()
->with('permissions')
->get()
->pluck('permissions')
->flatten()
->pluck('name')
->contains($permission);
}
Step 7: Middleware (Laravel Example)
public function handle($request, Closure $next, $permission)
{
if (!auth()->user()->hasPermission($permission)) {
abort(403, 'Unauthorized');
}
return $next($request);
}
Usage:
Route::get('/users', function () {
//
})->middleware('permission:user.read');
Advanced Best Practices
1. Use Naming Convention
resource.action
post.create
post.update
order.approve
2. Use Caching
Permission checks can be expensive:
Cache::remember('user_permissions_'.$user->id, 60, function () {
return ...
});
3. Avoid Role Explosion
❌ Bad:
- admin_us
- admin_eu
- admin_asia
✅ Better:
-
Use permissions like:
-
manage.users.us -
manage.users.eu
-
4. Dynamic Permission System (Recommended)
Store everything in DB so admin can manage:
- Create roles from UI
- Assign permissions dynamically
- No code changes needed 🔥
5. Use Packages (Laravel)
If using Laravel, use:
👉 Spatie Laravel Permission
Why?
- Production-ready
- Handles roles & permissions easily
- Built-in middleware
- Widely used in industry
Example Real-World Scenario
E-commerce System
| Role | Permissions |
|---|---|
| Admin | All |
| Manager | orders., products. |
| Staff | orders.read, orders.update |
| Customer | orders.create, orders.read |
Final Thoughts
If you remember ONE thing:
👉 Never check roles directly — always check permissions
Summary
✔ Use permissions as the source of truth
✔ Roles = grouping only
✔ Users = assigned roles
✔ Always check permissions
✔ Keep it dynamic and scalable
