TL;DR: Not every Laravel app needs the same authorization system. A blog needs one thing, a SaaS needs another, a fintech needs a third. This guide walks through 7 real-world authorization architectures I've seen in production Laravel apps, with code, trade-offs, and a decision matrix to pick the right one. The last one — the Hybrid Enterprise Engine — is what Laravel Permission Manager implements.
🔗 GitHub Repository · 📦 Packagist
📋 Table of Contents
- Why Architecture Matters
- Architecture #1: The Gate-Only Approach
- Architecture #2: The Spatie-Style RBAC
- Architecture #3: The Policy-Per-Model Pattern
- Architecture #4: Route-Based Authorization
- Architecture #5: Hierarchical RBAC
- Architecture #6: ABAC / Condition-Based Authorization
- Architecture #7: The Hybrid Enterprise Engine
- The Decision Matrix
- How to Migrate Between Architectures
- Final Thoughts
🤔 Why Architecture Matters
Walk into any Laravel team and ask "how do you do authorization?" — you'll get a different answer every time.
Some say "we use Gates." Others say "we use Spatie." Others whisper "we have a giant if tree in our User model."
Here's the uncomfortable truth: most Laravel teams don't choose an authorization architecture. They drift into one. A Gate::define here, a policy there, some middleware somewhere — and two years later they have a system nobody fully understands.
The cost of drifting:
- 🔐 Security gaps nobody noticed
- 🐛 Bugs that take weeks to debug
- 🧠 Knowledge silos — only one dev "knows how permissions work"
- 📈 Scalability walls that appear suddenly
This guide gives you 7 distinct architectures, each proven in production Laravel apps. By the end, you'll know exactly which one fits your project — and when to upgrade to the next.
🏛️ Architecture #1: The Gate-Only Approach
The pattern
Use Laravel's built-in Gate facade and Gate::define for every check. No package, no database tables, just closures.
// AuthServiceProvider.php
Gate::define('edit-post', function ($user, $post) {
return $user->id === $post->user_id;
});
Gate::define('delete-post', function ($user, $post) {
return $user->is_admin || $user->id === $post->user_id;
});
Gate::define('view-dashboard', function ($user) {
return $user->role === 'admin';
});
Usage:
if (Gate::allows('edit-post', $post)) { ... }
Where it works
- 📝 Blogs, portfolios, personal sites
- 🎯 Apps with fewer than ~15 permissions
- 👤 Solo developers who know the whole system
- 🧪 Prototypes and MVPs
Where it breaks
- ❌ No persistence: permissions live in code, can't be changed at runtime
- ❌ No admin UI: you can't give someone a permission without deploying
- ❌ Duplication: every gate is custom — no shared "editor" concept
- ❌ Audit impossibility: no record of who changed what
The breaking point
When someone asks "Can we add a manager role that has everything editors have plus posts.publish?" — you're rewriting dozens of gates.
🏛️ Architecture #2: The Spatie-Style RBAC
The pattern
Classic Role-Based Access Control stored in the database:
User ←[n:m]→ Role ←[n:m]→ Permission
This is the most popular pattern in the Laravel ecosystem, popularized by Spatie.
$user->assignRole('editor');
$role->givePermissionTo('posts.edit');
$user->hasPermissionTo('posts.edit'); // true
$user->can('edit posts'); // via Gate integration
Where it works
- 🏢 Most B2B SaaS applications
- 👥 Apps with clear role definitions
- 🔧 Apps where admins manage permissions via UI
Where it breaks
The moment requirements include exceptions or context, Spatie-style RBAC hits walls:
// Requirement: "Editor can edit posts, but not their own"
// Spatie has no way to express this cleanly
The breaking point
You end up with either:
-
Role explosion:
editor,editor-no-publish,editor-no-delete,senior-editor-drafts-only - Scattered logic: special-cases in controllers and policies that aren't in the DB
🏛️ Architecture #3: The Policy-Per-Model Pattern
The pattern
One policy class per Eloquent model, using Laravel's native authorization:
class PostPolicy
{
public function view($user, $post) { ... }
public function update($user, $post) {
return $user->id === $post->user_id
|| $user->hasRole('editor');
}
public function delete($user, $post) {
return $user->hasRole('admin');
}
}
Usage:
$user->can('update', $post);
Gate::authorize('delete', $post);
Where it works
- 🎯 Resource-heavy apps (CRUD on many models)
- 📚 Apps with clear model ownership
- 🏗️ Well-structured DDD codebases
Where it breaks
- ❌ No cross-cutting permissions:
reports.exportdoesn't belong to any model - ❌ Duplication: "is admin" logic repeats in 40 policies
- ❌ Drift: policies diverge from each other over time
- ❌ Untestable at scale: you need a test per policy method
The breaking point
"A user can only edit tasks in their department's projects, and only if they're assigned, and only if the project isn't archived."
Suddenly every policy needs the same 5-line check. You've built a shadow authorization system.
🏛️ Architecture #4: Route-Based Authorization
The pattern
Every Laravel route is a permission. Middleware checks the current route name against the user's permissions.
// Middleware
public function handle($request, Closure $next)
{
$route = Route::currentRouteName();
if (!$request->user()->hasPermissionTo($route)) {
abort(403);
}
return $next($request);
}
// Sync from Laravel's route table
php artisan permission:sync-routes
Where it works
- 🛣️ Admin panels with lots of routes
- 🔄 Apps where permissions map 1:1 to routes
- 🏭 Large teams where routes are the shared vocabulary
Where it breaks
- ❌ Can't express "edit this post" — it's binary
- ❌ Over-permissioned: having
users.editroute means editing any user - ❌ Route renames break permissions: refactor = permission migration
The breaking point
Any contextual rule — ownership, status, time-based access — doesn't fit. Route-based authorization is the floor, not the ceiling.
🏛️ Architecture #5: Hierarchical RBAC
The pattern
Classic RBAC with inheritance: roles extend other roles, permissions cascade.
$viewer->assignPermission('posts.view');
$editor->assignPermission('posts.edit');
$admin->assignPermission('users.manage');
$editor->inheritFrom('viewer');
$admin->inheritFrom('editor');
$user->assignRole('admin');
$user->hasPermissionTo('posts.view'); // ✅ inherited from viewer
Where it works
- 🏛️ Apps with clear org charts
- 🎓 Education platforms (student < TA < instructor < dean)
- 💼 Enterprise apps where "admin > manager > user" is intuitive
Where it breaks
- ❌ Cycle traps:
A → B → Abreaks apps without detection - ❌ Diamond problem: what if a role inherits from two parents with conflicting permissions?
- ❌ Cache invalidation: change a leaf role, all descendants must invalidate
- ❌ Deny semantics: inheritance can't express "admin, except for X"
The breaking point
"Admins inherit everything from editors, except posts.delete."
Pure hierarchy can't express negative inheritance. You need explicit deny rules layered on top.
🏛️ Architecture #6: ABAC / Condition-Based Authorization
The pattern
Attribute-Based Access Control. Decisions are made by evaluating attributes of the user, resource, and environment against declarative rules.
// Rule: "Users can edit posts they own, while in draft status"
PermissionCondition::create([
'permission_id' => $permission->id,
'conditions' => [
'all' => [
['field' => 'user.id', 'operator' => '=', 'value' => 'resource.user_id'],
['field' => 'resource.status', 'operator' => '=', 'value' => 'draft'],
],
],
]);
$user->canPermission('posts.edit', $post);
Where it works
- 🏦 Fintech, healthcare, compliance-heavy domains
- 🌐 Multi-tenant SaaS with shared data
- 🛒 Marketplaces where "owner" logic is everywhere
- 📜 Apps subject to SOC 2, ISO 27001, HIPAA
Where it breaks
- ❌ Complexity: simple "admin can do everything" becomes a rule
- ❌ Debug difficulty: why did a condition fail? What attribute was wrong?
- ❌ Performance: evaluating many conditions on every check
- ❌ Security: naive implementations with
eval()are dangerous
The breaking point
Pure ABAC is overkill for most apps. Used alone, it becomes unreadable. Used layered on top of RBAC, it's the gold standard.
🏛️ Architecture #7: The Hybrid Enterprise Engine
The pattern
Combine every architecture above into a layered authorization engine, where each check flows through a deterministic resolution order:
1. Super Admin bypass
2. Explicit User DENY ← highest priority
3. Explicit User ALLOW
4. Role DENY
5. Role ALLOW
6. Inherited Role permissions
7. ABAC / Condition evaluation
8. Wildcard resolution
9. Default: DENY ← lowest
Each layer answers a specific class of question:
| Layer | Answers |
|---|---|
| RBAC | "What kind of user is this?" |
| Direct permissions | "Any exceptions for this user?" |
| Hierarchy | "What do they inherit?" |
| ABAC | "Under what conditions?" |
| Teams | "In which tenant?" |
| Audit | "Who changed what, when?" |
| Explain | "Why was this denied?" |
The code
// Simple case — works like Spatie
$user->assignRole('editor');
$user->hasPermissionTo('posts.edit'); // ✅
// With hierarchy
$editor->inheritFrom('viewer');
$user->hasPermissionTo('posts.view'); // ✅ inherited
// With explicit deny
$user->denyPermissionTo('posts.delete');
$user->hasPermissionTo('posts.delete'); // ❌ deny wins
// With ABAC
$user->canPermission('posts.edit', $post); // ✅ owner + draft
// With teams
$user->assignRoleForTeam('admin', $acme);
$user->assignRoleForTeam('viewer', $globex);
// With debugging
PermissionManager::explain($user, 'posts.delete');
// ['allowed' => false, 'reason' => 'explicit_user_deny', ...]
Where it works
- 🏢 Enterprise SaaS with complex requirements
- 🌐 Multi-tenant platforms
- 🏦 Regulated industries
- 📈 Apps that will scale from MVP to enterprise
The trade-off
- ⚖️ Complexity: more moving parts than simpler architectures
- 📚 Learning curve: team needs to understand layers
- 🏗️ Setup: more migrations, more config
Why it's worth it
Every simpler architecture eventually migrates here. The hybrid engine isn't for day-one apps — it's for day-100 apps that have outgrown every simpler pattern.
🎯 The Decision Matrix
Pick your architecture based on your project's reality:
| Signal | Recommended Architecture |
|---|---|
| Solo dev, <10 permissions, blog/portfolio | #1 Gate-Only |
| Clear roles, <50 permissions, small SaaS | #2 Spatie-style RBAC |
| CRUD-heavy app with model ownership | #3 Policy-per-Model |
| Admin panel with many routes | #4 Route-Based |
| Clear hierarchy, few exceptions | #5 Hierarchical RBAC |
| Context-heavy rules (owner, status, dept) | #6 ABAC |
| Multi-tenant, regulated, or scaling SaaS | #7 Hybrid Enterprise |
Or, more pragmatically:
| If you've ever said... | You need... |
|---|---|
| "Just a simple role check" | #1 or #2 |
"We need a manager-no-delete role" |
#2 with deny rules, or #7 |
| "Users should only edit their own stuff" | #3 or #6 |
| "Admins have different rights per company" | #7 (with teams) |
| "We can't debug why access was denied" | #7 (with Explain API) |
| "We need an audit trail for compliance" | #7 (with audit logging) |
🔁 How to Migrate Between Architectures
The good news: migrations are usually additive, not replacements.
From #1 Gates to #2 RBAC
- Keep the gates
- Add the package
- Register permissions in DB
- Gradually migrate gates to
hasPermissionTo
From #2 RBAC to #5 Hierarchical
- Keep all roles and permissions
- Add
role_inheritstable - Add
inheritFrom()calls - Existing checks keep working
From #5 to #7 Hybrid
- Keep everything
- Add explicit deny, ABAC, teams, audit
- Resolution order handles conflicts automatically
The Laravel Permission Manager is designed so you can start at #2 and migrate toward #7 without breaking existing code:
// Day 1: Simple RBAC
$user->assignRole('editor');
// Day 100: Add deny
$user->denyPermissionTo('posts.delete');
// Day 200: Add hierarchy
$editor->inheritFrom('viewer');
// Day 300: Add ABAC
PermissionCondition::create([...]);
// Day 400: Add teams
$user->assignRoleForTeam('admin', $acme);
Every step is backward compatible. The architecture grows with your app.
💭 Final Thoughts
The biggest mistake I see in Laravel authorization isn't picking the wrong architecture — it's not picking one at all.
Teams drift. They mix Gates with policies with scattered middleware with a role table bolted on. Six months later, nobody can explain why user X can or cannot do Y.
The fix is simple:
- Know the 7 architectures. Each has its place.
- Pick one deliberately based on your project's real needs.
- Document the choice so the next developer knows why.
- Plan the migration path — most apps evolve from #1 → #2 → #7 over time.
If your app is at the point where simpler patterns are showing cracks — role explosion, scattered policies, impossible debug sessions, compliance questions — then the Hybrid Enterprise Engine is probably your next step.
composer require hosseinhezami/laravel-permission-manager
And remember: the best authorization architecture is the one your team can explain in a single sentence.
🔗 GitHub Repository · 📦 Packagist
Tags: #laravel #php #architecture #authorization #rbac #abac #security #webdev #saas #opensource #backend #softwarearchitecture
Top comments (0)