rennokki/guardian
Guardian is a Laravel package for managing roles and permissions. Define abilities, attach them to roles or users, and check access in routes, controllers, middleware, and Blade views through a clean, expressive API that keeps authorization logic simple, readable, and centralized.
Architecture fit
rennokki/guardian is a thin, Eloquent-native permissions layer: a permissions table, a polymorphic many-to-many pivot (model_has_permissions) so any model — not just User — can hold permissions, and a HasPermissions trait that adds givePermissionTo, revokePermissionTo, syncPermissions, hasPermissionTo and overrides Laravel's can()/cannot() so existing @can Blade directives and Gate checks continue to work unchanged. Wildcard support (e.g. posts.*) is included.
The important architectural distinction for us: this package models permissions only, not roles. There is no Role entity, no role hierarchy, no per-guard or per-team scoping, and no Role::assignPermission indirection. Roles have to be modeled either as naming conventions (admin, editor granted as permissions) or as a separate table we build. That is a deliberate simplicity trade — it maps cleanly onto small products with a handful of capability flags, and poorly onto anything with delegated administration, tenant-scoped grants, or a support team that needs to audit "why can this user do X."
It slots into the framework's authorization pipeline rather than alongside it, which is architecturally the right shape. The risk is that it occupies the authorization hot path while being a single-maintainer package with no release cadence.
Integration feasibility
Mechanically this is a low-effort integration: composer require, publish and run one migration, add the trait to the relevant models, seed permissions. Realistically 2–4 engineer-days for a greenfield surface: install, migration, seeding, backfill of any existing is_admin-style flags, gate/policy reconciliation, and test coverage. It does not require rewriting existing Gate::define or policy classes — the trait's can() override sits on top of them, though that override is exactly the thing to verify first, because it changes resolution order for every authorization check in the app.
Technical risk
| Risk | Severity | Note |
|---|---|---|
| Maintenance status | High | Zero dependents and a low star count imply near-zero production usage; assume no upstream fixes |
| Laravel/PHP version ceiling | High | Must verify composer.json constraints against our target (Laravel 11/12, PHP 8.3+); abandonware in the auth path is a compounding liability |
| Roles not modeled | Medium | We own the role abstraction, its caching, and its audit story |
| Authorization hot path | Medium | can() override + wildcard matching affects every check; a wildcard bug is a privilege-escalation bug |
| Cache coherence | Medium | Permission caching plus multi-process/Octane deployment is a stale-grant/stale-revoke vector |
| No audit trail | Medium | No built-in history of who granted what; likely a compliance gap for us |
| Security disclosure path | Medium | No security policy, no CVE history, no one to notify |
Key questions
illuminate/* and PHP constraints, and when was the last commit? Is the last release compatible with our target framework version?can() override respect gates defined after the trait boots, and how does it interact with Gate::before super-admin patterns?spatie/laravel-permission (roles + permissions + teams + broad adoption) — and what does Guardian actually buy us that Spatie does not?Verdict on technical merit: viable for a small, single-tenant app with a short horizon. Not defensible as the authorization core of a multi-tenant or long-lived product without a fork-and-own commitment.
Stack fit
Best fit is an existing Laravel app already using Eloquent models, standard Gate/Policy conventions, and a relational database — MySQL/Postgres both fine. The polymorphic pivot makes it convenient if permissions must attach to entities other than User (organizations, API clients, service accounts) without a second package.
Weak fit if we need: tenant-scoped permissions, role CRUD in an admin UI, delegated grant management, or an authorization model that a non-engineer auditor can read.
Migration path
Two realistic entry points:
users.is_admin, users.can_export): straightforward. Create permissions named after the flags, backfill the pivot in a single migration, leave the boolean columns in place read-only for one release as a rollback path, then drop them.spatie/laravel-permission: only worth doing if we are deliberately simplifying away from roles. The data model differs (Spatie has roles, model_has_roles, plus a separate model_has_permissions); we would flatten role→permission assignments into direct grants, which is lossy and hard to reverse. If we are already on Spatie, staying on Spatie is almost certainly the cheaper decision.Compatibility
Verify before committing: framework and PHP constraints; the pivot's model_type column length versus our longest namespaced model class (and whether we use Relation::morphMap); MySQL index byte limits on (permission_id, model_type, model_id); behavior under Laravel Octane and under queue workers with long-lived processes and a cached permission set; and whether the cache respects our configured cache store (Redis vs. array in tests).
Sequencing
can() override, wildcard matcher, and cache invalidation. Do not skip this; the package is too small to have a community that has found its edge cases.Maintenance
This is the dominant cost. Single maintainer, no dependents, no visible release cadence — effectively we are adopting a code sample, not a supported library. Budget for: a named internal owner, a forked copy under our org, and a standing item to re-evaluate on every Laravel major upgrade. The good news is the surface is small — a few hundred lines of trait and migration — so a fork is genuinely tractable rather than aspirational.
Support
No commercial support, no security disclosure process, no backfill of issues. Our support path is "the engineer who read the source." That is acceptable for a capability-flag system and unacceptable if this is customer-facing entitlement enforcement with contractual SLAs.
Scaling
Per-check cost is a cached lookup after the first hit, so read scaling is fine — provided the cache is enabled and shared. Watch for:
(model_type, model_id) explicitly.Failure modes
| Failure | Symptom | Detection |
|---|---|---|
| Stale cache after revoke | User retains access post-revocation | Canary test asserting revoke → immediate 403 |
| Wildcard over-grant | * or posts.* matches more than intended |
Unit tests on the matcher with adversarial names (posts_admin, posts.) |
| Permission deleted, pivot orphaned | Silent no-op grants, confusing admin UI | FK constraint with cascade, plus a reconciliation job |
| Morph map drift | Rows keyed on FQCN, code uses alias (or vice versa) | Assert stored model_type values in a test |
can() override regression |
Gates/policies stop firing or fire out of order | Full policy test suite green before cutover |
| Upstream abandonment mid-upgrade | Blocked Laravel upgrade | Recorded in ADR; fork is the mitigation |
Ramp-up
Low. A mid-level Laravel engineer is productive in under a day — the API is five methods and one trait. The non-obvious learning is the can() override semantics and the caching behavior; document both in the ADR so the next engineer does not rediscover them during an incident.
Bottom line for the roadmap: cheap to try, cheap to abandon, and cheap to own — but only if we treat it as internal code we are adopting rather than a dependency we are consuming. If the product needs roles, tenant scoping, or an auditable grant history within the next 12 months, choose spatie/laravel-permission now and skip the migration; the switching cost later is higher than the incremental integration cost today.
How can I help you explore Laravel packages today?