Comparison
Checked on 2026-10-28 against these exact versions. Every cell links to the doc or source line it is based on.
”—” means the feature does not exist in that version (searched in docs and src/).
| Version checked | |
|---|---|
| laravel-access-profiles | 0.1.0 (Packagist) |
spatie/laravel-permission | 8.3.2 (current major 8) (Packagist) |
silber/bouncer | v1.0.4 (Packagist) |
santigarcor/laratrust | 8.5.6 (Packagist) |
| Laravel Gate + policies | Laravel 13.x, built in (docs) |
Short links used below: SP = spatie/laravel-permission@8.3.2, BO = silber/bouncer@v1.0.4, LT = santigarcor/laratrust@8.5.6.
Feature table
| Row | access-profiles 0.1.0 | spatie/laravel-permission 8.3.2 | silber/bouncer 1.0.4 | laratrust 8.5.6 | Plain Gate / policies |
|---|---|---|---|---|---|
| Runtime deps | PHP 8.3+ + 8 Illuminate packages: auth, cache, console, contracts, database, events, support, view (composer.json) | illuminate/* + spatie/laravel-package-tools (composer.json) | illuminate/{auth,cache,container,contracts,database} only (composer.json) | the whole laravel/framework + kkszymanowski/traitor (composer.json) | none |
| Multi-tenant scope | Two levels: tenant + scopes inside it, Eloquent models via morphs (ownership, tests) | One team_id dimension (teams) | One tenant id per request via Bouncer::scope()->to() (README “Multi-tenancy”) | One team dimension, passed per call (teams) | You write it |
| One assignment → a set of scopes | Yes: one assignment + N scope rows, or whole tenant (builder, tests) | — (one role row per team) | — (abilities can target one model instance, to('edit', $post), not a scope set) | — (one role row per team) | You write it |
| Deny | Yes, deny wins (resolver, tests) | — | Yes, forbid() (README) | — | You write it |
| Expiry / validity window | valid_from / valid_until; cache TTL capped at the next boundary (cache, window tests, TTL tests) | — | — | — | You write it |
| Grantor + reason audit | Assignment grant/revoke metadata + write events; actor/reason may be null, including system writes with no actor (actor, builder, tests) | — (role events exist, opt-in, no grantor/reason: events) | — (no events in src/) | — (model events roleAdded etc., no grantor/reason: events) | You write it |
| Appointment title | title column (builder, tests) | — (model_has_roles has a composite primary key and no extra columns: migration) | — | — | You write it |
| “Who can do X here?” | Access::holders('x', $scope) — deny, validity, scope and conditions applied; request tokens excluded (query, tests) | User::permission('x') scope, for the current global team (basic usage) | By role only: whereIs('admin') (source); not by ability | whereHasPermission('x', $team) (source) | — (loop over users) |
| Octane / queue global state | No global current tenant: target passed per check; memo is a scoped binding (provider, resolver, memo tests) | Global team id via setPermissionsTeamId(); must be set per request/job; optional Octane reset listener (cache) | Static scope (Models::$scope, source) set by middleware | None for checks: team is a call argument | None |
| Sanctum / API tokens | Optional tokenCan intersection by duck typing for model-backed tokens, no Sanctum dependency (resolver, config, tests, deps) | — (Passport client-credentials guide only: passport) | — | — | tokenCan() by hand |
| Wildcard | Write-time expansion against the catalogue (billing.*) (catalogue, tests) | Optional read-time wildcard (wildcard) | everything(), toManage(Model) (README) | hasPermission('admin.*') (roles and permissions) | — |
| Cache | Per-request memo + versioned per-tenant cache using standard store operations, no tags (cache, versions, config, tests) | One cached permission set, 24 h default, reset on change (cache) | Per-request; cross-request optional; full refresh uses cache tags when available (README “Cache”) | Optional, on in production by default (config) | — |
| Owner / super-admin | No built-in bypass; access requires a matching assignment (resolver, tests) | Recommends Gate::before (super-admin) | everything() | role check | Gate::before |
| Customer-editable profiles per tenant | Template + tenant copy (based_on_id) (builder, tests) | Team roles (team_id on roles) | Scoped roles | Roles are global; teams scope the assignment (team_id only on role_user / permission_user: migration) | — |
| Laravel versions | 12.69+, 13.30+ (within majors 12 and 13; composer.json) | 12, 13 | 11, 12, 13 | 10, 11, 12, 13 | — |
| Maintenance | New (changelog) | Active: 8.3.0 (2026-07-03), 8.3.1 + 8.3.2 (2026-10-19) | Slow: v1.0.2 (2024-03-14), v1.0.3 (2025-02-24), v1.0.4 (2026-03-18) | Active: 8.5.4 (2026-02-23), 8.5.5 (2026-03-06), 8.5.6 (2026-10-22) | Laravel core |
Maintenance dates for the other packages come from the Packagist metadata linked in the version table.
Reading the table honestly
- spatie/laravel-permission is the safe default for a single-tenant or one-dimension-team app. It has the biggest community. Our package is worth it only when you need the rows where Spatie shows ”—”: scope sets, deny, validity, grantor/title audit.
- Bouncer is the closest in spirit (it has deny). It lacks validity, audit and a two-level scope, keeps tenant state static, and releases about once a year.
- Laratrust passes the team explicitly (good for queues) but pulls in the whole framework package and has no deny or expiry.
- Plain Gate + policies is enough when roles are few and fixed in code. Our package works next to the Gate, not instead of it: host policies call
canAccess(); it does not register Gate abilities (provider, policy example test).