PHPackages                             kirchdev/laravel-pbac - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. kirchdev/laravel-pbac

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

kirchdev/laravel-pbac
=====================

Policy-based access control for Laravel: roles, permissions, organisation-scoped authorization, Gate integration, and a decision cache.

v0.1.0(2w ago)06↓100%[1 PRs](https://github.com/kirchDev/laravel-pbac/pulls)MITPHPPHP ^8.4CI passing

Since May 21Pushed 1w agoCompare

[ Source](https://github.com/kirchDev/laravel-pbac)[ Packagist](https://packagist.org/packages/kirchdev/laravel-pbac)[ Docs](https://github.com/kirchDev/laravel-pbac)[ RSS](/packages/kirchdev-laravel-pbac/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (10)Versions (4)Used By (0)

🛡️ laravel-pbac
===============

[](#️-laravel-pbac)

**Policy-based access control for Laravel — roles, permissions, multi-tenant scoping, decision tracing, and native `Gate` integration.**

[![Latest Version on Packagist](https://camo.githubusercontent.com/cc2b34e63c6049c999828037dd6101bc28fb6a8eee27a0f7339f305eba88f378/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6b697263686465762f6c61726176656c2d706261632e7376673f7374796c653d666c61742d73717561726526636f6c6f723d346634366535)](https://packagist.org/packages/kirchdev/laravel-pbac)[![Total Downloads](https://camo.githubusercontent.com/9ea2933056c1b36d1af5c1d57b893424fee610937134dc02627ff33e3e9195e8/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6b697263686465762f6c61726176656c2d706261632e7376673f7374796c653d666c61742d73717561726526636f6c6f723d346634366535)](https://packagist.org/packages/kirchdev/laravel-pbac)[![Tests](https://camo.githubusercontent.com/f12f94a9322a5714412ff15512d0a1bfa27e1fafbf5fe38281c1925db2b5997f/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6b697263684465762f6c61726176656c2d706261632f63692e796d6c3f6272616e63683d6d61696e267374796c653d666c61742d737175617265266c6162656c3d7465737473)](https://github.com/kirchDev/laravel-pbac/actions/workflows/ci.yml)[![PHP Version](https://camo.githubusercontent.com/3c13d94568572aafd8f88ff875d3371d621bc56e97bbf9a209a297dafd1316db/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f6b697263686465762f6c61726176656c2d706261632f7068703f7374796c653d666c61742d73717561726526636f6c6f723d383939336265)](https://packagist.org/packages/kirchdev/laravel-pbac)[![Laravel Version](https://camo.githubusercontent.com/dfab9fc2045880a1eee97e0f261c2e92b4202ffd6056600b6e1f5a04ccdb8fe1/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f6b697263686465762f6c61726176656c2d706261632f696c6c756d696e617465253246737570706f72743f7374796c653d666c61742d737175617265266c6162656c3d6c61726176656c26636f6c6f723d666632643230)](https://packagist.org/packages/kirchdev/laravel-pbac)[![License: MIT](https://camo.githubusercontent.com/f6618dfe1b17c4468451ed2d70ae447f1d4345d4af0f1a539796044bd6f83fa6/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6b697263686465762f6c61726176656c2d706261632e7376673f7374796c653d666c61742d73717561726526636f6c6f723d313062393831)](LICENSE)

---

```
Pbac::withOrganisation($org->id, fn () => $user->can('members.invite')); // ✅
```

That's it. Tenant-aware authorization in one line, native Laravel `Gate` semantics, no manual scope plumbing.

✨ Features
----------

[](#-features)

- **🎭 Roles &amp; permissions** — plain Eloquent models you can swap out for your own (UUID / ULID / int keys).
- **🏢 Organisation/tenant scoping** — first-class, with a pluggable `OrganisationResolver`. Scopes never bleed across tenants.
- **🚪 Native `Gate` integration** — `$user->can()`, `Gate::allows()`, `Gate::inspect()` all Just Work, with fallback to native Laravel gates.
- **⚡ Per-request decision cache** — repeated checks within a request are free. Auto-invalidates on role/permission mutations.
- **🔍 Decision trace** — opt-in audit trail of *why* a check returned what it did. Redacted in production by default.
- **🚀 Octane-aware** — optional reset listeners on `RequestTerminated`, `TaskTerminated`, `TickTerminated`. No stale state across requests.
- **🧰 Heavy configuration** — model / table / column / key types all overridable. UUID setups supported out of the box.
- **🧪 Library-grade** — Pest 4 + Testbench, no host app needed.

📦 Installation
--------------

[](#-installation)

```
composer require kirchdev/laravel-pbac
```

Publish and run the migrations:

```
php artisan vendor:publish --tag=pbac-migrations
php artisan migrate
```

Optionally publish the config:

```
php artisan vendor:publish --tag=pbac-config
```

🚀 Quick start
-------------

[](#-quick-start)

Add the `HasRoles` trait to whichever model should be authorizable:

```
use Illuminate\Foundation\Auth\User as Authenticatable;
use KirchDev\Pbac\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
}
```

Create roles, attach permissions, assign, check:

```
use KirchDev\Pbac\Models\{Permission, Role};

$role = Role::create(['name' => 'editor']);
$role->permissions()->attach(
    Permission::create(['name' => 'posts.update'])
);

$user->assignRole($role);

$user->can('posts.update');     // ✅ true
Gate::allows('posts.update');   // ✅ true (same plumbing)
Gate::inspect('posts.update');  // ✅ Response with trace (if enabled)
```

🏢 Multi-tenant authorization
----------------------------

[](#-multi-tenant-authorization)

Enable organisation scoping:

```
// config/pbac.php
'organisation' => [
    'enabled' => true,
    'resolver' => \KirchDev\Pbac\Organisation\DefaultOrganisationResolver::class,
],
```

Scope authorization for the current request:

```
use KirchDev\Pbac\Facades\Pbac;

Pbac::withOrganisation($organisation->id, function () use ($user) {
    $user->can('members.invite'); // checked against org-bound roles
    $user->can('billing.view');   // …same scope
});

// Global checks — no active org
Pbac::withoutOrganisation(fn () => $user->can('admin.impersonate'));
```

The decision cache resets on scope enter/exit, so checks **never bleed across tenants**.

### Assigning global roles when org scoping is enabled

[](#assigning-global-roles-when-org-scoping-is-enabled)

To prevent silent mis-targeting, role **mutations** by name refuse to resolve a global role unless the caller signals intent. Pass `global: true`, or wrap the call in `Pbac::withoutOrganisation()` and assign by `Role` instance:

```
$user->assignRole('superadmin', global: true);
$user->removeRole('superadmin', global: true);
$user->syncRoles(['superadmin', 'support_lead'], global: true);
$user->hasRole('superadmin', global: true);

// Equivalent for arbitrary mixed batches:
$role = Role::findOrCreate('superadmin');
Pbac::withoutOrganisation(fn () => $user->assignRole($role));
```

Inside an active organisation scope, `$user->assignRole('owner')` resolves the org-scoped row only — global rows with the same name are deliberately invisible to mutations without the explicit flag.

Bring your own resolver (e.g. backed by a tenancy package or route binding):

```
final class TenantRouteResolver implements \KirchDev\Pbac\Contracts\OrganisationResolver
{
    public function getOrganisationId(): int|string|null
    {
        return request()->route('organisation')?->getKey();
    }
    // …setOrganisationId, clearOrganisationId
}
```

Wire it via `pbac.organisation.resolver`.

🔍 Decision trace
----------------

[](#-decision-trace)

Wondering *why* a permission check returned what it did? Turn on tracing:

```
// config/pbac.php
'trace' => [
    'enabled' => true,
    // null → auto: redact when APP_ENV=production AND APP_DEBUG=false
    // true|false → forced
    'redact' => null,
    'log' => [
        'enabled' => false,           // structured logging via Laravel's logger
        'channel' => null,            // null = default channel
        'level' => 'info',
        'on' => 'deny',               // or 'all'
    ],
],
```

`Gate::inspect()` carries the decision's reason code via `Response::message()`:

```
$response = Gate::inspect('posts.update', $post);

$response->allowed();  // bool
$response->message();  // 'pbac.role_permission_allowed' | 'pbac.no_matching_role_permission' | …
```

For the human-readable trace, reach for the last decision through the `Pbac` facade:

```
use KirchDev\Pbac\Facades\Pbac;

$user->can('posts.update', $post);

Pbac::lastDecision()?->trace()->visible();   // structured entries
Pbac::lastDecision()?->trace()->formatted(); // 'role_permission_query(allowed=1, targeted=1) → role_permission_allowed'
```

Production redacts trace context arrays by default (step names stay; values are stripped). Opt in to the full trace per request when you need it — e.g. for an admin debug route:

```
Pbac::withUnredactedTrace(function () use ($user, $post) {
    $user->can('posts.update', $post);

    return Pbac::lastDecision()?->trace()->formatted(); // unredacted
});
```

🧹 Cascade behaviour on delete
-----------------------------

[](#-cascade-behaviour-on-delete)

Foreign keys are deliberately set to `ON DELETE CASCADE` so the indexes never carry stale grants or assignments. Mark this on your operational checklist:

When you delete…These rows go away automaticallyA `Role`All `role_has_permissions` rows for that role + all `model_has_roles` rows.A `Permission`All `role_has_permissions` rows referencing it.A host model (e.g. `User`) row**Not** automatic. `model_has_roles` rows on the morph side are orphaned.The host-model side is polymorphic (`model_type` + `model_id`), so no FK enforces it. Hook your model's `deleting`/`deleted` events or run a periodic prune job if user/team deletions are part of your normal flow. If you need an audit trail of historical grants/assignments, capture it **before** deletion — once the cascade fires, the rows are gone.

⚙️ Configuration highlights
---------------------------

[](#️-configuration-highlights)

`config/pbac.php` is heavily parameterised — see the file for inline docs. Most common knobs:

KeyWhat it controls`models.*`Swap any of the 4 Eloquent models (Role / Permission / RoleAssignment / RolePermission).`table_names.*`Override defaults if they collide with existing tables.`keys.*``id` / `uuid` / `ulid` for primary keys, model morphs, target morphs, org FK. Set **before** migrations.`column_names.*`Pivot and morph key column names (handy for UUID setups).`organisation.enabled` / `.resolver`Toggle multi-tenancy, plug a custom resolver.`gate.fallback_to_laravel_gates`Whether unmatched abilities fall back to native Laravel gates.`trace.enabled`Capture per-decision explanations. Redacted in prod by default.`cache.decision_store`Decision cache backend (`request` by default).`register_octane_reset_listener`Reset scoped state at Octane worker boundaries.🔁 Migrating from `spatie/laravel-permission`
--------------------------------------------

[](#-migrating-from-spatielaravel-permission)

Coming from `spatie/laravel-permission`? See the dedicated guide for schema, API, and multi-tenancy differences plus a copy-pasteable data-migration script: [docs/migration-from-spatie-laravel-permission.md](docs/migration-from-spatie-laravel-permission.md).

🧪 Testing
---------

[](#-testing)

```
composer install
composer test       # Pest 4
composer pint       # Laravel Pint (test mode)
composer larastan   # Larastan / PHPStan
```

The test suite runs via Testbench + in-memory SQLite — no host app required.

🤝 Contributing
--------------

[](#-contributing)

PRs welcome. Conventional Commits required (enforced via commitlint). Husky runs Pint + Larastan + oxlint + oxfmt on `git commit`, so you can mostly forget about style.

Tip

Run `pnpm check:fix` (Node tooling) and `composer pint:fix` (PHP) before pushing — CI will catch what husky missed.

🛣️ Versioning
-------------

[](#️-versioning)

[Semantic Versioning](https://semver.org/). Release notes in [CHANGELOG.md](CHANGELOG.md) — managed by [release-please](https://github.com/googleapis/release-please).

📄 License
---------

[](#-license)

[MIT](LICENSE) © [Titus Kirch](https://github.com/TitusKirch/) / [IT-Dienstleistungen Titus Kirch](https://kirch.dev)

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance97

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 87.5% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

19d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/fd76235e5967a18294c81e065828051b5fee3f3e71b7a9f17e90ebbbffb95d1f?d=identicon)[TitusKirch](/maintainers/TitusKirch)

---

Top Contributors

[![TitusKirch](https://avatars.githubusercontent.com/u/16943133?v=4)](https://github.com/TitusKirch "TitusKirch (63 commits)")[![claude](https://avatars.githubusercontent.com/u/81847?v=4)](https://github.com/claude "claude (5 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (3 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")

---

Tags

authorizationgatelaravelmulti-tenantpbacpermissionsrbacroleslaravelauthorizationrolespermissionsrbacmulti-tenantgatepbac

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/kirchdev-laravel-pbac/health.svg)

```
[![Health](https://phpackages.com/badges/kirchdev-laravel-pbac/health.svg)](https://phpackages.com/packages/kirchdev-laravel-pbac)
```

###  Alternatives

[spatie/laravel-permission

Permission handling for Laravel 12 and up

12.9k98.0M1.3k](/packages/spatie-laravel-permission)[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k14.1M120](/packages/laravel-pulse)[hasinhayder/tyro

Tyro - The ultimate Authentication, Authorization, and Role &amp; Privilege Management solution for Laravel 12 &amp; 13

6753.6k5](/packages/hasinhayder-tyro)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9732.3M121](/packages/roots-acorn)[wnikk/laravel-access-rules

Simple system of ACR (access control rules) for Laravel, with roles, groups, unlimited inheritance and possibility of multiplayer use.

103.7k1](/packages/wnikk-laravel-access-rules)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
