bbs-lab / nova-permission
Manage roles and permissions in Laravel Nova with a visual permission builder, per-instance (granular) permissions and a super-admin override, built on spatie/laravel-permission.
Requires
- php: ^8.2
- laravel/nova: ^4.0 || ^5.0
- spatie/laravel-package-tools: ^1.92.4
- spatie/laravel-permission: ^6.17.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0 || ^10.0 || ^11.0
- orchestra/workbench: ^9.0 || ^10.0 || ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- pestphp/pest-plugin-mutate: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
This package is auto-updated.
Last update: 2026-08-02 20:23:45 UTC
README
Based on spatie/permission, this tool gives you ability to manage roles and permission. The tool provides permission builder.
Contents
Requirements
- PHP
^8.2 - Laravel Nova
^4.0 || ^5.0 - spatie/laravel-permission
^6.0 - Laravel
^11.0 || ^12.0 || ^13.0
Both Nova majors are exercised in CI. Note that Nova 4 (through its inertiajs/inertia-laravel
dependency) tops out at PHP 8.4 and Laravel 11; on PHP 8.5 or Laravel 12+, use Nova 5. Composer
resolves the right combination for you.
Installation
To get started, you will need to install the following dependencies
composer require bbs-lab/nova-permission
The service provider will automatically get registered. Or you may manually add the service provider in your config/app.php file:
'providers' => [ // ... BBSLab\NovaPermission\NovaPermissionServiceProvider::class, ],
Publish Spatie's permission migration (the base permissions/roles tables):
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="permission-migrations"
This package's own migrations (which add the group + polymorphic authorizable columns and the override_permission flag) run automatically with php artisan migrate — you do not need to publish them.
You can publish the config files with:
php artisan vendor:publish --tag="nova-permission-config"
This publishes two files. config/permission.php is Spatie's standard configuration — the only change this package needs is to point the two model bindings at its own models (which implement the package contracts):
// config/permission.php (only the keys this package changes; the rest is Spatie's default) return [ 'models' => [ /* * When using the "HasPermissions" trait from this package, we need to know which * Eloquent model should be used to retrieve your permissions. Of course, it * is often just the "Permission" model but you may use whatever you like. * * The model you want to use as a Permission model needs to implement the * `BBSLab\NovaPermission\Contracts\Permission` contract. */ 'permission' => BBSLab\NovaPermission\Models\Permission::class, /* * When using the "HasRoles" trait from this package, we need to know which * Eloquent model should be used to retrieve your roles. Of course, it * is often just the "Role" model but you may use whatever you like. * * The model you want to use as a Role model needs to implement the * `BBSLab\NovaPermission\Contracts\Role` contract. */ 'role' => BBSLab\NovaPermission\Models\Role::class, ], // `table_names`, `column_names`, `display_permission_in_exception` and `cache` // keep Spatie's defaults — leave them as published unless you have a reason to change them. ];
// config/nova-permission.php return [ // Resources whose individual records can carry their own permissions // (see "Granular permissions" below). Leave empty if you only use // general, model-wide permissions. 'authorizable_models' => [ // \App\Nova\Post::class, ], // Resources excluded from `nova-permission:generate`. 'generate_without_resources' => [ \Laravel\Nova\Actions\ActionResource::class, ], // Authorization gate cache. Disabled by default — enable it only when the // read volume justifies it and you accept the staleness window (entries are // invalidated on permission save/delete). `ttl` is the lifetime in seconds. 'cache' => [ 'enabled' => env('NOVA_PERMISSION_CACHE', false), 'ttl' => env('NOVA_PERMISSION_CACHE_TTL', 60 * 60), ], ];
Then create the tables by running the migrations (Spatie's published migration plus this package's auto-registered ones):
php artisan migrate
Usage
You must register the tool with Nova. This is typically done in the tools method of the NovaServiceProvider:
// app/Providers/NovaServiceProvider.php <?php use BBSLab\NovaPermission\PermissionBuilder; use Laravel\Nova\NovaApplicationServiceProvider; class NovaServiceProvider extends NovaApplicationServiceProvider { // .. public function tools() { return [ // ... PermissionBuilder::make(), // You may add some access control PermissionBuilder::make()->canSee(function ($request) { return $request->user()->hasRole('admin'); }), ]; } }
Prepare your User model
On your authenticatable model, use this package's HasRoles trait (not Spatie's) and implement the CanOverridePermission contract. The package trait adds the per-instance hasPermissionToOnModel() method the policies rely on, and the contract powers the super-admin (override_permission) feature:
namespace App\Models; use BBSLab\NovaPermission\Contracts\CanOverridePermission; use BBSLab\NovaPermission\Traits\HasRoles; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable implements CanOverridePermission { use HasRoles; }
Important
Use BBSLab\NovaPermission\Traits\HasRoles, not Spatie\Permission\Traits\HasRoles. The Nova policies call hasPermissionToOnModel(), which exists only on the package trait — with Spatie's trait every protected resource throws Call to undefined method.
Generate permissions
The tool allow to generate resource permissions. Your resources must implement BBSLab\NovaPermission\Contracts\HasAbilities and define the public static $permissionsForAbilities variable :
namespace App\Nova; use BBSLab\NovaPermission\Contracts\HasAbilities; use BBSLab\NovaPermission\Traits\Authorizable; class Post extends Resource implements HasAbilities { use Authorizable; public static $permissionsForAbilities = [ // 'policy action' => 'display name' 'create' => 'create post', ]; }
This configuration will generate the following permission:
[
'name' => 'create post',
'group' => 'Post', // class basename of the model
'guard_name' => 'web', // the nova guard or default auth guard
]
You may generate permission from the permission builder tool with the Generate permissions button or the Artisan command:
php artisan nova-permission:generate
Protect resources
You may use authorization policies and extend the provided BBSLab\NovaPermission\Policies\Policy class
namespace App\Policies; use App\Models\Post; use BBSLab\NovaPermission\Policies\Policy; use Illuminate\Contracts\Auth\Access\Authorizable; class PostPolicy extends Policy { protected function model(): string { return Post::class; } }
Important
You must create a policy for each model you want to protect else the default permissions will be applied!
The base Policy class takes care of the following actions for you :
- viewAny
- view
- create
- update
- replicate
- delete
- restore
- forceDelete
You are free to add or update methods.
Granular permissions
Permissions come in two flavours:
- General — a model-wide permission (the row's
authorizableisnull). "Can this user view posts?" - Granular — a permission scoped to one specific record via a polymorphic
authorizablerelation. "Can this user view this post?"
The two are resolved with an instance-override rule:
An instance that carries a granular permission is governed by that permission alone. An instance with no granular permission falls back to the general permission.
So granting view service generally lets a user view every service, until a granular view service is attached to a particular service — from then on that one service is gated by the granular grant only, while the rest keep following the general one.
To make a model's individual records authorizable, implement BBSLab\NovaPermission\Contracts\HasAuthorizations and use the BBSLab\NovaPermission\Traits\Authorizations trait:
namespace App\Models; use BBSLab\NovaPermission\Contracts\HasAuthorizations; use BBSLab\NovaPermission\Traits\Authorizations; use Illuminate\Database\Eloquent\Model; class Service extends Model implements HasAuthorizations { use Authorizations; }
Register the Nova resource in config/nova-permission.php so the builder lets you attach permissions to individual records:
'authorizable_models' => [ \App\Nova\Service::class, ],
You can now create a permission scoped to a specific service:
Tip
To create the granular permission automatically whenever a record is created, use an observer.
Because the base Policy already routes per-instance abilities (view, update, delete, …) through hasPermissionToOnModel(), a policy that maps the ability name to the permission needs no extra code — the instance-override rule is applied for you. You only override a policy method when the granular permission has a different name than the ability:
namespace App\Policies; use App\Models\Service; use BBSLab\NovaPermission\Policies\Policy; use Illuminate\Contracts\Auth\Access\Authorizable; use Illuminate\Database\Eloquent\Model; class ServicePolicy extends Policy { protected function model(): string { return Service::class; } // The parameter type must stay `Model` to match the base Policy signature // (PHP forbids narrowing it to Service); the instance is your Service. public function view(Authorizable $user, Model $service): ?bool { if ($user->hasPermissionToOnModel('view secret service', $service)) { return true; } return null; } }
Filtering an index by permission
To list only the records a user may act on, apply the authorize query scope (provided by the Authorizations trait). It mirrors the instance-override rule and honours override_permission roles:
Service::query()->authorize($request->user(), 'view service')->get();
Caching
Gate checks are not cached by default. Enable the cache from config/nova-permission.php (or the NOVA_PERMISSION_CACHE env flag) once read volume justifies it. Cached entries are invalidated when a permission is saved or deleted; see the config comments for the staleness caveats.
Super admin
You may want to create a role as super admin. You can do that using the override_permission attribute.
You may prevent override_permission attribute modification by using the BBSLab\NovaPermission\Resources\Role::canSeeOverridePermmission method:
// in a service provider BBSLab\NovaPermission\Resources\Role::canSeeOverridePermmission(function (Request $request) { // implement your logic });
Testing
The package ships a runnable Nova workbench that doubles as a manual playground and a demo of every feature:
composer serve # boots Nova at http://127.0.0.1:8000/nova
Log in with admin@laravel.com / password (also seeded: writer@laravel.com, reader@laravel.com, same password). The workbench seeds roles, a Post and Service resource showcasing general vs granular permissions, and Nova impersonation so you can switch between the seeded users without logging out.
The quality gate:
composer test # Pest composer test-coverage # Pest with 100% line coverage on src/ composer analyse # PHPStan level 8 composer format # Pint
Front-end work additionally uses npm run package (Prettier + ESLint + Vite build).
Changelog
Please see CHANGELOG for more information on what has changed recently, and UPGRADE for the 1.x → 2.0 migration.
Security
If you discover any security related issues, please email paris@big-boss-studio.com instead of using the issue tracker.
Contributing
Please see CONTRIBUTING for details.
Credits
License
The MIT License (MIT). Please see License File for more information.


