Access Profiles
v0.1.2 Open source · MIT

Different permissions in different parts of a tenant.

Scoped access profiles for multi-tenant Laravel apps. Deny wins. Every grant records who gave it and why.

composer require seisigmasrl/laravel-access-profiles
View on GitHub

PHP 8.3+ · Laravel 12.69+ or 13.30+

grant.php
Assignment
$assignment = Access::grant('reviewer')->to($user)->in($team)    ->onlyScopes([$site, $otherSite])->titled('Review cover')    ->validFrom(now())->validUntil(now()->addMonths(6))    ->by($admin)->because('Cover leave')->save();
One assignment. Selected scopes. A validity window. A reason.
The problem

Same person.
Same tenant.
Different access.

Manager in two sites, read-only in a third. A broad grant in billing, except approving payments. Access needs to describe where a person can act, and where it stops.

A holder, an assignment, and scopes within a tenantOne person has manager access in site 1 and site 2, and read-only access in site 3. Payment approval is denied in site 4. Assignments record validity dates, grantor and reason. A matching deny always wins.01 / HOLDERSame personYour Eloquent model02 / ASSIGNMENTProfile + scope setWhole tenant or selected scopesValid from → valid untilWho granted accessWhy access was granted03 / TENANT → SCOPESTeamYour Eloquent modelSite 1ManagerSite 2ManagerSite 3Read-onlySite 4 · billingPayment approvalDeny wins
The scope map · different assignments can cover different scopes.No matching rule → access denied.

Access can be temporary or appointed.An external auditor until a date. A title such as “Board treasurer”.

A broad grant can have one hole.Everything in billing, except approving payments.

“Who can approve payments in site 4?”That must be a query, not a loop over every user.

“Who gave this person access, and why?”The answer belongs with the assignment.

Follow the scope map

Explicit boundaries.
Predictable decisions.

Tenants, scopes and holders are your own Eloquent models. Profiles combine allow and deny rules. Assignments connect them.

01

Access per scope set, not per tenant.

One assignment covers the whole tenant or a chosen list of scopes.

Assignment → scope cellsgrant.php selects the scopes.
02

Deny wins. Default is no.

Any matching deny beats every allow, in any order. With no rule, the answer is false.

The denied cellprofile.php combines allow and deny.
03

Access that ends on time, without a scheduler.

Start dates are inclusive, end dates exclusive. The cache TTL stops at the next boundary.

The assignment’s datesgrant.php sets the validity window.
04

Who, why, and when — on every grant.

Each assignment stores the grantor and the reason. Revoked rows stay as history. Write events fire after commit.

The assignment’s recordgrant.php records who and why.
query.php retrieves history.
05

“Who can do X here?” is one query.

Access::holders() applies deny, scope, validity and conditions, and returns a builder you can paginate.

From a scope back to holdersquery.php asks who has access.
06

Safe grants by default.

Guards reject self-assignment and escalation. Scopes must belong to the tenant. Rejections write nothing.

The tenant boundarygrant.php names the tenant, scopes and grantor.
07

No global tenant state, no owner bypass.

Every check takes a target. Safe with Octane and queued jobs. Owners get a full profile like anyone else.

A specific scope, every timecheck.php passes the target.
08

Small footprint, tested docs.

PHP and 8 illuminate/* packages only. Every README example has a behaviour test.

Your models, Laravel’s foundationEach value point has a test in the repo.
Code walk-through

From a profile to an answer.

Access Profiles works next to Laravel’s Gate. $user->can() and Gate keep their normal behaviour; your policies call canAccess().

01 / Define

Allow a group. Deny an action.

A profile combines rules. A matching deny wins.

profile.php
Profile
$profile = Access::profile('reviewer')->in($team)
    ->allow('billing.*')->deny('billing.invoice.approve')
    ->by($admin)->because('Keep approvals separate')->save();
02 / Grant

Choose where and when.

Assign the profile to a holder, with selected scopes, dates and a reason.

grant.php
Assignment
$assignment = Access::grant('reviewer')->to($user)->in($team)
    ->onlyScopes([$site, $otherSite])->titled('Review cover')
    ->validFrom(now())->validUntil(now()->addMonths(6))
    ->by($admin)->because('Cover leave')->save();
03 / Check

Ask about a target.

Check access, or explain the decision.

check.php
Decision
$allowed = Access::for($user)->can('billing.invoice.view', $site);
$sameAnswer = $user->canAccess('billing.invoice.view', $site);
$decision = Access::for($user)->explain('billing.invoice.approve', $site);
04 / Query

Find holders. Read history.

Query access with scope, validity, conditions and deny rules applied.

query.php
Holders + history
$holders = Access::holders('billing.invoice.view', $site);
$count = Access::holdersQuery('billing.invoice.view', $site)->count();
$history = Access::history($user, $team);
05 / Render

Use the same check in Blade.

Pass the permission and its target.

Blade
View
@canAccess('billing.invoice.view', $site)
View invoices
@endcanAccess
Comparison

Choose for the access model you have.

Access Profiles is new. Scope sets, deny rules and grant history are reasons to consider it. Other models may need less.

Checked on 2026-10-28. Scroll inside the table to compare packages; row labels stay visible.

Versions: Access Profiles 0.1.0 · spatie/laravel-permission 8.3.2 · silber/bouncer v1.0.4 · santigarcor/laratrust 8.5.6 · Laravel 13.x Gate
CapabilityAccess Profilesspatie/laravel-permissionBouncerLaratrustGate + policies
Multi-tenant scopeTenant + scopes inside itOne team dimensionOne tenant id per requestOne team, passed per callYou write it
One assignment → a set of scopesYes———You write it
DenyYes, deny wins—Yes, forbid()—You write it
Validity windowvalid_from / valid_until———You write it
Grantor + reason auditYes, plus write events———You write it
“Who can do X here?”Access::holders()User::permission() for the current teamBy role onlywhereHasPermission()Loop over users
Global tenant stateNone; target per checkGlobal team idStatic scopeNoneNone
Owner / super-adminNo bypassGate::beforeeverything()Role checkGate::before
Laravel versions12.69+, 13.30+12, 1311, 12, 1310, 11, 12, 13—
MaintenanceNewActiveSlow (about one release a year)ActiveLaravel core

An honest reading

spatie/laravel-permission is the safe default for one tenant or one team level, with the biggest community. Bouncer is the closest in spirit (it has deny). Laratrust passes the team explicitly. Plain Gate + policies is enough when roles are few and fixed.

Full comparison with sources
Fit matters

When not to use it

  • One tenant or one flat team level → consider spatie/laravel-permission.
  • A few fixed roles → Laravel's Gates and policies may be enough.
  • Record-level rules → keep them in a policy.
  • An admin UI out of the box → build your own screens.
  • A mature, proven package today → this package is new. Review it before relying on it for access control.
Install

Start with your permission catalogue.

Choose table names before migrating.

PHP 8.3+
Laravel 12.69+ or 13.30+
Namespace: Seisigma\AccessProfiles

  1. composer require seisigmasrl/laravel-access-profiles
  2. php artisan vendor:publish --tag=access-profiles-config
  3. php artisan vendor:publish --tag=access-profiles-migrations
  4. Configure the permission catalogue and templates. Choose table names before migrating.

  5. php artisan migrate
  6. php artisan access-profiles:sync-templates
FAQ

A few boundaries,
made explicit.

Does it replace Laravel's Gate?

No. Gate and $user->can() keep their normal behaviour. Call canAccess() or Access::for() from a policy.

Is there a super-admin or owner bypass?

No. An owner gets a full profile like everyone else.

Is it safe with Octane and queues?

There is no global tenant state. Every check takes a target. Jobs re-check when they run.

Do I need a scheduler for expiry?

No. The cache stops at the next validity boundary.

Does it ship an admin UI?

No. Build your own screens.

Does it work with Sanctum tokens?

Optional. With token_intersection, a token can only narrow access. No Sanctum dependency.

Can I move from another permission package?

Yes. Write a BackfillSource and run access-profiles:backfill --dry-run first.

Access Profiles

A tenant. Its scopes.
Access with boundaries.

Allow and deny. Grant and revoke. Keep the reason.

composer require seisigmasrl/laravel-access-profiles
Read the docs