masoudi/nova-acl
masoudi/nova-acl adds role-based access control to Laravel Nova. Manage roles and permissions, assign them to users, and restrict access to Nova resources, actions, and tools through an admin-friendly interface designed for Laravel projects using Nova.
This is a 2-star, archived, zero-dependent package. Treat it as a starting point or a reference implementation, not a production dependency. Before installing anything, open the repo and answer three questions in this order:
composer.json — what does it require? If it pins laravel/nova to ^3.0 or ^4.0, it will not work with current Nova. Check illuminate/* constraints too.src/ — is there a NovaServiceProvider, a ToolServiceProvider, or plain Resource classes? This tells you whether it registers itself or whether you must register resources manually in NovaServiceProvider::resources().database/migrations/ — does it create its own roles/permissions tables, or does it assume spatie/laravel-permission already exists? This is the single most important thing to determine, because getting it wrong means table-name collisions or missing columns.Install (knowing it's archived):
composer require masoudi/nova-acl
If the package ships migrations and a config file, publish them before running anything:
php artisan vendor:publish --provider="Masoudi\NovaAcl\NovaAclServiceProvider"
php artisan migrate
If vendor:publish lists nothing, there is no service provider — you'll need to register the resource classes by hand in app/Nova/NovaServiceProvider.php.
First use case — the one thing this kind of package is built for: assign a role to a user from inside Nova, and have Gate respect it. Concretely: log into Nova, open a User detail page, find a role field or a "Roles" relationship panel, attach admin, then confirm Gate::allows('viewAny', Post::class) flips from false to true. Do this once on a throwaway user before wiring it into anything else — it validates the whole chain (model trait, migration, policy integration, cache).
If Nova shows no new menu entries after install, the package registers resources only when Nova::resources() picks them up — clear the Nova cache:
php artisan nova:cache-clear
php artisan optimize:clear
Assume it wraps spatie/laravel-permission unless proven otherwise. Most "Nova ACL" packages are thin UI layers over Spatie. Confirm by grepping for HasRoles and Spatie\Permission in src/:
grep -r "Spatie\|HasRoles\|HasPermissions" vendor/masoudi/nova-acl/src
If it does, you get Spatie's API for free and the package is only worth it for the resource screens — you can drop the package later and keep the data.
Attach the trait to your User model (this is almost never done for you):
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
}
Wire permissions into policies — not middleware. The day-to-day pattern is:
class PostPolicy
{
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('posts.view');
}
public function update(User $user, Post $post): bool
{
return $user->hasPermissionTo('posts.update');
}
}
Then register Gate::before once in AuthServiceProvider for a super-admin bypass. This is the pattern nearly every Nova admin app ends up with:
public function boot()
{
Gate::before(function ($user, $ability) {
return $user->hasRole('super-admin') ? true : null;
});
}
Returning null (not false) is what lets normal policies still run. Returning false silently kills every permission for non-admins — a very common bug.
Seed roles rather than clicking them in. Whatever the Nova screens do, roles are config, and config should be in code so a fresh environment matches production:
// database/seeders/RoleSeeder.php
$admin = Role::create(['name' => 'admin']);
$admin->givePermissionTo(['posts.view', 'posts.create', 'posts.update', 'posts.delete']);
User::where('email', '[email protected]')->first()->assignRole('admin');
Integrate with Nova fields for the per-record case. If the package exposes a Nova field (e.g. a role selector), place it explicitly rather than relying on auto-discovery:
public function fields(NovaRequest $request)
{
return [
Text::make('Name')->sortable(),
// role assignment on the user resource
Role::make('Roles')->hideFromIndex(),
];
}
Guard everything at the API boundary too. Nova's ACL rarely protects your front-end routes. If you expose the same models over an API, mirror the policy checks — $this->authorize('update', $post) in the controller — because permissions stored in the DB mean nothing without a Gate::authorize call somewhere.
When the package doesn't do what you need, wrap it, don't fork it. Since it's archived, put your customization in your own app/Nova/Resources/Role.php and your own policies. Extend, don't vendor-edit — you'll never get upstream fixes, and a composer update will erase your work if you're editing inside vendor/.
illuminate/support ^11.0 constraints). Pin your Nova version and audit the composer.json constraints before running composer update on anything else in the project.roles/permissions migrations and you also have spatie/laravel-permission's migrations, you will get "table already exists" or, worse, silently mismatched columns. Decide on one schema before running migrate.hasPermissionTo() returning stale false after a role change is almost always the cache. php artisan permission:cache-reset (or app(PermissionRegistrar::class)->forgetCachedPermissions()) fixes it. Never change roles in a tinker session and expect web requests to see it immediately.Gate::before returns are load-bearing. true = always allow, false = always deny, null = defer to the policy. Returning a role check's boolean directly (return $user->hasRole('admin');) forces false on all non-admins and bypasses every policy you wrote. Use the ternary-to-null form shown above.web vs api). If your API users authenticate via Sanctum and your roles were seeded with guard_name = 'web', hasPermissionTo('x') on the API guard returns false. Either set guard_name explicitly everywhere or use hasPermissionTo('x', 'web'). Check the config's guard key.hasPermissionTo() can hit the cache and, on miss, the DB. In a Nova index of 100 users each rendering a role column, that's painful. Eager-load: User::with('roles')->... in the resource's indexQuery.App\Models\User table by foreign key, it must run after your users migration. If it runs first, MySQL may error or silently create no FK. Check timestamps in vendor/.../migrations/ and bump the filename if needed.config/nova-acl.php is published, check for keys that toggle features (e.g. register_resources, super_admin_role, cache_ttl). A published config with a hardcoded role name that doesn't match your seeded role is a silent no-op.NovaServiceProvider::resources(), your own Role/Permission Nova resources, your own policies, and a Gate::before in AuthServiceProvider. All four live outside vendor/ and survive updates (or the absence of them).class_uses(User::class); (2) record exists — Role::where('name','admin')->first(); (3) guard matches the authenticated guard; (4) cache cleared; (5) Gate::forUser($user)->allows(...) in tinker to isolate the UI layer from the auth layer. If tinker says yes and Nova says no, the problem is the package's UI, not your ACL — which, given it's archived, you may just want to replace with two hand-written Nova resources.How can I help you explore Laravel packages today?