eminiarts/nova-permissions
A Laravel Nova tool for managing roles and permissions with a clean Nova UI. Create and edit roles and permissions, assign them to users, and integrate with spatie/laravel-permission for flexible access control.
Check the matrix before you type composer require. eminiarts/nova-permissions 1.0.6 (2019-09-09) is the last release, and its constraints are the ceiling: spatie/laravel-permission v3, PHP 7.2+, Laravel 6, Nova of that era. Open its composer.json next to your app's and decide now whether you are installing it or reading it in order to fork it. On anything modern (Laravel 9+, PHP 8, Nova 4/5, Spatie v5/6) the honest plan is: install into a scratch branch to read the source, then copy the field into app/Nova/Fields/.
Get Spatie healthy first, alone. This package is a UI — it adds no authorization engine. Before touching it, confirm the spatie migrations have run, HasRoles is on your User model, and php artisan permission:cache-reset executes cleanly. If those three things aren't true, every symptom you see afterwards will be misattributed to the field.
Seed before you render. An empty permission table produces an empty matrix, which looks exactly like a broken install. Seed a realistic set first, and seed it in the naming convention the field depends on:
foreach (['users.view', 'users.create', 'posts.view', 'posts.publish'] as $name) {
Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']);
}
Read the field class end-to-end — it's essentially one file. Ten minutes gets you: how options are resolved, how the current value is hydrated, how the group headings are derived, and what it calls on save (syncPermissions, presumably). Read that, then the shipped Role/Permission Nova resources, then composer.json. Reading it beats debugging it from the outside, and it's the difference between "I own this" and "this owns me."
Wire it onto a Role resource you control. Verify the exact class namespace against the README — names shifted between minor releases:
use Eminiarts\NovaPermissions\Nova\GroupedPermissions;
public function fields(Request $request)
{
return [
Text::make('Name')->rules('required'),
GroupedPermissions::make('Permissions'),
];
}
First use case, end to end: open /nova/resources/roles/{id}, tick a group, save, then verify from tinker:
$role->permissions->pluck('name', 'guard_name');
$role->hasPermissionTo('posts.publish');
Rendering checkboxes but failing to persist is almost never a UI bug — it's a guard-name mismatch or sync semantics. Both are covered below.
Ship your own App\Nova\Role, don't decorate Nova's default. Register it in NovaServiceProvider::$resources and keep the field isolated there. This is the single cheapest decision that makes future upgrades (or your eventual fork) survivable.
Treat resource.action as a load-bearing convention. Grouping is a literal explode('.') on the first segment. users.view and users.create group under "Users"; view users groups under nothing, and posts.publish splits cleanly. Standardize the convention app-wide before adoption, then enforce it in the seeder — not in code review.
Generate permissions from one source of truth, idempotently. Drive them from a config array, a policies scan, or an artisan command that runs firstOrCreate. Re-running the seeder must be a no-op. That's what keeps the matrix stable across environments and deploys — hand-maintained permission lists drift within one sprint.
Pair the checkbox edit with a policy. The field renders checkboxes; it does not stop a crafted request. Add a RolePolicy gating create/update on a permission like manage roles, and exclude manage roles itself from the matrix — otherwise a delegated admin can grant themselves anything with two clicks. This is the most common way this package gets deployed insecurely.
Keep the matrix on Roles, not Users. The field works on a User resource if you use direct permissions, but that doubles your authorization surface and makes "why does this person have access?" unanswerable. Roles-only is the habit; direct grants are the exception.
Multi-guard: verify the filter explicitly. If you run web + api, confirm the field filters options by the guard you expect. A mixed matrix is unreadable at best and produces cross-guard grants at worst. Same for Spatie teams: this edits a role's permission set, not the team-role-user relation — that stays separate work.
Test saved state, not screenshots. Seed, actingAs a real admin, hit the resource endpoint, then assert both sides:
$this->assertTrue($role->fresh()->hasPermissionTo('users.view'));
$this->assertFalse($role->fresh()->hasPermissionTo('users.delete'));
That covers roughly 90% of what actually breaks here, and it survives Nova upgrades that break your browser-based assumptions.
Free "who can do what" screen. Render the same field ->readonly() on a detail view for non-technical admins. No custom page, no new endpoint.
Put cache invalidation in the deploy script. Any deploy that changes the permission list should be followed by php artisan permission:cache-reset. Wire it in next to your migrations/seeder step and this entire class of "the new permission doesn't show up" bugs disappears.
composer require is the start of a fork, not a dependency. Zero dependents, one release in years. Pin 1.0.6 exactly — never ^1.0 — or vendor the field into app/Nova/Fields/GroupedPermissions.php with a comment linking upstream. It's ~150 lines; owning it removes the maintenance risk entirely and lets you fix Spatie API drift on your own schedule.
Spatie v3 → v5/6 is the hidden trap. The package targets v3. On newer Spatie, audit every call the field makes: hasPermissionTo, syncPermissions, the permissions relation shape, and how the permission cache is flushed. Guard handling and the relation's return shape have genuinely changed between majors — most calls are stable, those two are not. Confirm before you trust the matrix.
Guard mismatch is the #1 silent failure. Checkboxes render, save returns 200, $role->permissions looks populated — but hasPermissionTo() is false because the permissions landed on web while the role or your check uses api. Fix: set guard_name explicitly in the seeder and on the role, and verify the field isn't filtering on a hardcoded guard. Debug with $role->permissions->pluck('name', 'guard_name').
sync silently deletes out-of-band grants. If the field syncs the relation, any permission granted by a seeder, another package, or a one-off tinker command is removed on the next Nova save. This bites weeks into production, usually during an incident. Decide explicitly: either "the matrix is the source of truth" (document it loudly) or override the fill logic to merge (syncWithoutDetaching semantics).
Permission cache. Spatie caches aggressively, so after a permission-list change the matrix can show stale or missing rows. When "the new permission isn't in Nova," run php artisan permission:cache-reset before reading any code.
Renaming a permission is a breaking change. post.view → posts.view renames a group heading and invalidates every gate, policy, @can, canSee, and canRun referencing the old string. Grep the whole app before renaming, or you get a permissions screen that lies.
Nova's own authorization short-circuits your debugging. If the field isn't visible at all, it's canSee, the Role resource's authorizedToView, or your test user genuinely lacking the manage-roles permission — not the package. Always test as a real admin, not a factory user with zero roles.
Scale degrades hard past ~100 permissions. A few hundred checkboxes is slow to render and unusable to operate. Past that, switch to per-resource tabs or a different tool — pushing this field harder is the wrong investment.
Extend the grouping, don't fight it. Overriding the field's grouping method to map raw names to friendly labels ("Posts — Publish", "Users — View") is about ten lines and materially improves admin UX over the raw explode('.') default. It's also the cleanest place to inject localization for multi-language admin panels.
How can I help you explore Laravel packages today?