PHPackages                             webrek/laravel-mongo-permission - 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. [Database &amp; ORM](/categories/database)
4. /
5. webrek/laravel-mongo-permission

ActiveLibrary[Database &amp; ORM](/categories/database)

webrek/laravel-mongo-permission
===============================

Role and permission management for Laravel with a MongoDB backend

v1.3.0(3w ago)001MITPHPPHP ^8.1CI passing

Since May 18Pushed 3w agoCompare

[ Source](https://github.com/webrek/laravel-mongo-permission)[ Packagist](https://packagist.org/packages/webrek/laravel-mongo-permission)[ RSS](/packages/webrek-laravel-mongo-permission/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (9)Versions (5)Used By (1)

webrek/laravel-mongo-permission
===============================

[](#webreklaravel-mongo-permission)

Role and permission management for Laravel — MongoDB native.

API-compatible with `spatie/laravel-permission` for the methods most people use day to day, but the data model, queries, and cache strategy are designed around MongoDB.

Requirements
------------

[](#requirements)

DependencyVersionsPHP8.1, 8.2, 8.3Laravel10.x, 11.x, 12.xMongoDB server7.x`mongodb/laravel-mongodb``^4.0` | `^5.0`PHP `mongodb` extensionrequiredCI runs every release against the full PHP × Laravel matrix above, excluding the combinations Laravel itself does not support (Laravel 11 and 12 require PHP 8.2+).

Install
-------

[](#install)

```
composer require webrek/laravel-mongo-permission
php artisan vendor:publish --tag=permission-config
php artisan permission:create-indexes
```

Quick start
-----------

[](#quick-start)

```
use Webrek\MongoPermission\Models\Permission;
use Webrek\MongoPermission\Models\Role;
use Webrek\MongoPermission\Traits\HasRoles;

class User extends Authenticatable {
    use HasRoles;
}

Permission::create(['name' => 'edit articles']);
$role = Role::create(['name' => 'editor']);
$role->givePermissionTo('edit articles');

$user->assignRole('editor');
$user->hasPermissionTo('edit articles'); // true
```

Status
------

[](#status)

v1.0 — ready for production. Covers single + multi-tenant Laravel apps, multi-guard auth, request-scoped + persistent caching, eight lifecycle events, wildcard permissions, route middleware, Blade directives, Gate integration, and a full set of Artisan commands.

Why this vs `spatie/laravel-permission`?
----------------------------------------

[](#why-this-vs-spatielaravel-permission)

This package targets Laravel apps whose user model and auth surface live in MongoDB. If your stack is SQL, use `spatie/laravel-permission`— it is mature, widely adopted, and SQL is what it was built for.

If your stack is MongoDB:

- **No pivot tables.** Role and permission grants live as embedded subdocuments on the user document. One read per permission check, no joins, no `model_has_roles` / `role_has_permissions` ceremony.
- **Multi-tenant from day one.** Every read and write threads `team_id` through models, cache keys, and events. Tenant isolation is a config flag (`strict_team_isolation`), not a third-party concern.
- **Events that already know your tenant.** `RoleAttached`, `PermissionAttached` and their counterparts carry `team_id` and `guard` in the payload — your audit listener does not have to re-query.
- **Cache shape matches the access pattern.** Slug arrays are keyed by `(user_id, team_id)`; catalog by `(guard, team_id)`. Lookups are O(1) against the exact tuple the check uses.
- **Wildcards on by default.** Segment-based matching with a configurable separator, greedy trailing `*`, exact interior `*`.
- **MongoDB-native indexes.** Compound uniques on `(name, guard_name, team_id)`, multikey on `permission_ids` for reverse queries. Generated by `permission:create-indexes`.

The public API mimics Spatie's on purpose so existing knowledge transfers. The internals do not.

Caching
-------

[](#caching)

`hasPermissionTo` and `hasRole` consult an in-memory + Laravel Cache layer keyed by `(user_id, team_id)`. Mutations through `assignRole`, `removeRole`, `givePermissionTo`, `revokePermissionTo`, and `syncRoles`/`syncPermissions` invalidate the affected keys via package events.

Flush manually if needed:

```
php artisan permission:cache-reset
```

Configure the cache store and key namespace in `config/permission.php`under the `cache` key.

**Known limitation:** changing the permission catalog of a role (e.g. `$role->givePermissionTo(...)` / `$role->revokePermissionTo(...)`) does not automatically invalidate the cached slug arrays of every user holding that role — invalidation is per-user, fired by per-user attach or detach events. Run `permission:cache-reset` after bulk role-catalog edits, or rebuild the cache user-by-user.

Multi-guard
-----------

[](#multi-guard)

Every `Role` and `Permission` is scoped by `guard_name`. The same name can exist in multiple guards independently. The guard for an operation resolves in this order:

1. Explicit argument: `$user->hasRole('admin', 'api')`
2. `protected string $guard_name` property on the user model
3. `auth.defaults.guard`
4. `config('permission.default_guard')`

Mismatched guards on `assignRole` / `givePermissionTo` calls with model instances throw `GuardDoesNotMatch`.

Multi-tenant teams
------------------

[](#multi-tenant-teams)

Set `permission.teams = true` (default) and either call `setPermissionsTeamId('your-team-id')` manually or supply a closure in `permission.team_resolver`:

```
'team_resolver' => fn () =>
    request()->user()?->current_team_id
    ?? request()->header('X-Team-Id'),
```

Assignments made while a team is active are scoped to that team. Reads honor the active team. Setting `permission.strict_team_isolation = true`disables the "team\_id = null is global" fallback.

Expiring grants
---------------

[](#expiring-grants)

Roles and permissions can be granted with an expiry. The grant stays on the user document but stops counting toward checks the moment `now()` passes the `expires_at` timestamp — even if the cache was warmed before the expiry.

```
$user->assignRoleUntil('admin', now()->addHours(2));
$user->givePermissionToUntil('publish posts', now()->addDays(7));

$user->hasRole('admin');                  // true for two hours
$user->hasPermissionTo('publish posts');  // true for seven days

// After the expiry passes:
$user->hasRole('admin');                  // false
```

Expired subdocs are not removed automatically. Run the prune command on a schedule (or ad-hoc) to garbage-collect them and free space on user documents:

```
php artisan permission:prune-expired
php artisan permission:prune-expired --dry-run
php artisan permission:prune-expired --user-model="App\\Models\\User"
```

A role granted with an expiry propagates that expiry to every permission reached through the role — once the role assignment expires, those permissions stop counting too.

Role hierarchy
--------------

[](#role-hierarchy)

Roles can inherit permissions from other roles. A user assigned a role transparently gets every permission attached to that role and to every role in its ancestor chain.

```
$viewer = Role::create(['name' => 'viewer']);
$viewer->givePermissionTo('view articles');

$editor = Role::create(['name' => 'editor']);
$editor->givePermissionTo('edit articles');
$editor->inheritsFrom($viewer);   // editor now grants view + edit

$admin = Role::create(['name' => 'admin']);
$admin->inheritsFrom($editor);    // admin now grants view + edit transitively

$user->assignRole('admin');
$user->hasPermissionTo('view articles');   // true
$user->hasDirectPermission('view articles'); // false (transitive)
```

Inheritance is multi-parent: a role can extend several parents at once. Diamonds resolve cleanly — a permission reached through more than one path counts once.

**Cycle detection.** `inheritsFrom` throws `Webrek\MongoPermission\Exceptions\RoleHierarchyCycle` if the new edge would create a loop. `RoleHierarchyTooDeep` fires when the total chain length would exceed `permission.role_hierarchy_max_depth`(default `5`).

**Detaching a parent.** `$role->stopsInheritingFrom($parent)` drops the edge. The package fires `RoleParentChanged` (with `action = 'attached'` or `'detached'`) and flushes the registrar cache so every affected user picks up the change on the next read.

Wildcard permissions
--------------------

[](#wildcard-permissions)

`enable_wildcard_permission` defaults to `true`. Patterns use `.` as the separator (configurable via `permission.wildcard_separator`). A trailing `*` is greedy and matches all remaining segments; interior `*` matches exactly one segment; a sole `*` matches any non-empty name.

```
Permission::create(['name' => 'posts.*']);
$user->givePermissionTo('posts.*');
$user->hasPermissionTo('posts.edit');         // true
$user->hasPermissionTo('posts.edit.own');     // true
```

Middleware
----------

[](#middleware)

```
Route::get('/admin', ...)->middleware('role:admin');
Route::get('/edit',  ...)->middleware('permission:edit articles');
Route::get('/x',     ...)->middleware('role_or_permission:admin|edit articles');
Route::get('/teams/{team}/admin', ...)
    ->middleware(['team-context:team', 'role:admin']);
```

Denied requests throw `Webrek\MongoPermission\Exceptions\UnauthorizedException`(HTTP 403). Register a custom exception handler if you want a different response shape.

Blade directives
----------------

[](#blade-directives)

```
@role('admin') ... @endrole
@hasanyrole('admin|editor') ... @endhasanyrole
@hasallroles('admin|editor') ... @endhasallroles
@unlessrole('guest') ... @endunlessrole

@permission('edit articles') ... @endpermission
@haspermission('edit articles') ... @endhaspermission
@hasanypermission('edit|delete') ... @endhasanypermission

@can('edit articles') ... @endcan   {{-- native Laravel, routed via Gate::before --}}
```

Gate integration
----------------

[](#gate-integration)

The package installs a `Gate::before` hook so `$user->can()`, `@can`, and controller `authorize()` calls consult `hasPermissionTo`. Unknown permission names return `null` from the hook so the rest of the Gate stack (Policies, manually-defined gates) still runs.

Events
------

[](#events)

The package dispatches eight lifecycle events. Subscribe to them for audit logging, cache extensions, or custom side-effects.

EventPayload`RoleCreated``Role $role``RoleDeleted``Role $role``PermissionCreated``Permission $permission``PermissionDeleted``Permission $permission``RoleAttached``mixed $user, Role $role, ?string $teamId, string $guard``RoleDetached``mixed $user, Role $role, ?string $teamId, string $guard``PermissionAttached``mixed $model, Permission $permission, ?string $teamId, string $guard``PermissionDetached``mixed $model, Permission $permission, ?string $teamId, string $guard``PermissionAttached` / `PermissionDetached` carry the model that received the change — a `User` instance or a `Role` instance — so listeners can branch on the case. All `*Attached` / `*Detached`events include the active `team_id` and `guard`, enabling per-tenant auditing without re-querying.

All event classes live in `Webrek\MongoPermission\Events`.

Artisan commands
----------------

[](#artisan-commands)

```
php artisan permission:create-indexes
php artisan permission:create-role admin [--guard=web] [perm1 perm2 ...]
php artisan permission:create-permission "edit articles" [--guard=web]
php artisan permission:show [--guard=web] [--team=...]
php artisan permission:cache-reset
php artisan permission:prune-expired [--user-model=...] [--dry-run]
php artisan permission:list-users {role} [--permission=...] [--guard=...] [--team=...]
php artisan permission:check {user_id} {permission} [--guard=...] [--team=...]
php artisan permission:migrate-from-spatie [--connection=mysql] [--match-by=email] [--skip-users] [--force] [--dry-run]
```

Migrating from spatie/laravel-permission
----------------------------------------

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

If you are coming from an existing [`spatie/laravel-permission`](https://github.com/spatie/laravel-permission)deployment, `permission:migrate-from-spatie` reads the canonical spatie tables out of a SQL connection and writes the equivalent documents into the package's Mongo collections.

```
# Dry-run against your "spatie" SQL connection
php artisan permission:migrate-from-spatie --connection=spatie --dry-run

# Real run, matching SQL users to Mongo users by email
php artisan permission:migrate-from-spatie --connection=spatie

# Roles and permissions only, no user assignments
php artisan permission:migrate-from-spatie --connection=spatie --skip-users

# Overwrite Mongo roles/permissions that already exist with the same (name, guard, team)
php artisan permission:migrate-from-spatie --connection=spatie --force
```

The command reads these five spatie tables: `permissions`, `roles`, `role_has_permissions`, `model_has_roles`, `model_has_permissions`, plus the SQL `users` table (override with `--sql-user-table=`) to match SQL user ids to Mongo user documents.

Matching defaults to `email` (override with `--match-by=`). Any SQL user without a corresponding Mongo user is reported but does not break the run. The migration is idempotent: a second run skips roles and permissions that already exist with the same (name, guard, team) tuple.

Configuration
-------------

[](#configuration)

Published to `config/permission.php`:

KeyDefaultDescription`models.role``Webrek\MongoPermission\Models\Role`Concrete Role class — swap to extend`models.permission``Webrek\MongoPermission\Models\Permission`Concrete Permission class — swap to extend`collection_names.roles``'roles'`Mongo collection for roles`collection_names.permissions``'permissions'`Mongo collection for permissions`guard_names``['web', 'api']`Guards the package will validate against`default_guard``'web'`Fallback when no guard can be resolved`teams``true`Enable multi-tenant scoping by `team_id``team_resolver``fn () => null`Closure that returns the active `team_id``strict_team_isolation``false`If true, `team_id = null` no longer matches every team`enable_wildcard_permission``true`Toggle wildcard matching in `hasPermissionTo``wildcard_separator``'.'`Segment separator for wildcard patterns`throw_on_missing_permission``true`Throw `PermissionDoesNotExist` for unknown names instead of returning `false``handle_unauthorized``true`Let middleware throw 403 `UnauthorizedException``cache.store``'default'`Laravel Cache store for slug/catalog keys`cache.key``'mongo-permission'`Namespace prefix for all package cache keys`cache.expiration_time``null``null` = forever (trust event-driven invalidation)Testing locally
---------------

[](#testing-locally)

```
docker compose up -d mongo
docker compose run --rm php composer install
docker compose run --rm php vendor/bin/phpunit
docker compose run --rm php vendor/bin/phpstan analyse --memory-limit=1G
```

The repository includes a `docker-compose.yml` that boots MongoDB 7 with a healthcheck so the test suite starts as soon as the database is ready. No PHP or Mongo install on the host is required.

For consumer apps, the package ships an assertion trait you can drop into your TestCase to test role and permission state with expressive assertions:

```
use Webrek\MongoPermission\Testing\MongoPermissionAssertions;

class FooTest extends TestCase
{
    use MongoPermissionAssertions;

    public function test_admin_can_edit(): void
    {
        $this->assertUserHasRole($user, 'admin');
        $this->assertUserHasPermission($user, 'edit articles');
        $this->assertUserHasDirectPermission($user, 'publish');
        $this->assertRoleHasPermission($role, 'view');
    }
}
```

License
-------

[](#license)

MIT

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance95

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% 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

Every ~0 days

Total

4

Last Release

22d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/7d8deca81629993819087597b5ad7695976b02e3d014f038e26e985f35f569de?d=identicon)[webrek](/maintainers/webrek)

---

Top Contributors

[![webrek](https://avatars.githubusercontent.com/u/5001338?v=4)](https://github.com/webrek "webrek (51 commits)")

---

Tags

laravelauthorizationaclrolespermissionsrbacmongodbmulti-tenantwildcard

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/webrek-laravel-mongo-permission/health.svg)

```
[![Health](https://phpackages.com/badges/webrek-laravel-mongo-permission/health.svg)](https://phpackages.com/packages/webrek-laravel-mongo-permission)
```

###  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)[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.4k51.0M7.4k](/packages/larastan-larastan)[silber/bouncer

Eloquent roles and abilities.

3.6k4.6M27](/packages/silber-bouncer)[laravel/ai

The official AI SDK for Laravel.

9782.1M153](/packages/laravel-ai)[illuminate/database

The Illuminate Database package.

3.0k54.1M11.0k](/packages/illuminate-database)

PHPackages © 2026

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