You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
New major of the spatie/laravel-permission wrapper. It's the RBAC foundation every next-gen package with controllers relies on, so its permission-registration and policy-override APIs come first; then views and controllers for roles and permissions.
Built to the next-gen general rules: CI with test and coverage badges, full docs for people and agents, Laravel Boost guideline and skill, Javaabu codestyle, PHP 8.4 / Laravel 12+ (Laravel packages) or TypeScript (JS packages), Dhivehi and RTL, Blade and Inertia React for views, overridable controllers and policies with javaabu/permissions.
All work is on the next-gen branch (branch alias dev-next-gen → 3.x-dev); one PR into main at the end.
Architecture
Javaabu\Permissions\Permissions becomes a facade over a singleton PermissionRegistry. Packages call it from their service provider's boot(); nothing touches the database until you sync.
Registry: holds permission groups (group key = the existing permissions.model column), each with a label, permissions (name => English description), optional guards and default role grants. It also holds the default role definitions and package policies.
Sync: PermissionSyncer + php artisan permissions:sync (--prune, --guard=) + Javaabu\Permissions\Database\Seeders\PermissionsSeeder. It is idempotent: it creates or updates each permission for each guard, creates roles, and grants defaults. Grants only add permissions, except '*' roles, which are synced to everything. --prune removes registered-group permissions that are no longer registered.
Policies: Permissions::policy(Model::class, Policy::class). These are applied to the Gate after boot, and only where no policy is already defined. The order is: config('permissions.policies') wins, then the app's own Gate::policy(), then the package default. ResourcePolicy is the base policy: it maps viewAny/view/create/update/delete/restore/forceDelete/viewTrash to view_x/edit_x/delete_x/force_delete_x, supports custom abilities through an $abilities map, and supports optional ownership (edit_others_x).
Multi-auth: registered permissions go to every guard in config('permissions.guards') (default: the app's default guard), unless a group names its own guards. Helpers: Permissions::guardFor($user), Permissions::check($permissions, $guard), $user->anyPermission($permissions, $guard), @anypermission($permissions, $guard), Role::forGuard(). No App\Models\User or web assumptions; events take any Model as the causer.
Translations: descriptions and labels are English strings used as translation keys (__() at display time, through translated_description and group_label), so a package ships them in its lang/dv.json.
Controllers (Overridable controllers, policies and default permissions (RBAC, multi-auth) #11): RolesController (web and JSON API), bound through config('permissions.controllers'). Routes are registered by Route::roles([...guard, prefix, middleware, name]). RolePolicy and the roles permissions are registered through the API above. Views render Blade or Inertia depending on config('permissions.views').
Dropping the hard dependencies on javaabu/activitylog and javaabu/helpers. activitylog v2 (wave 4) will depend on permissions, and helpers requires javaabu/settings ^1.7, while settings v2 (wave 3) will also depend on permissions. Keeping either dependency would make apps on the next-gen stack unresolvable.
Activity logging of role/permission changes becomes optional: it is feature-detected through spatie/laravel-activitylog.
Role no longer implements AdminModel; it gets its own search scope.
Both changes go in docs/upgrading.md.
Open question: should Role keep AdminModel through an optional subclass?
Factories move from the global Database\Factories namespace to Javaabu\Permissions\Database\Factories, so they don't collide with the app's.
HasRoles::updateRole() no longer calls get_setting('default_role'). It takes an explicit default role and causer.
Do this in your service provider's boot(). Nothing touches the database until the app runs the sync.
useJavaabu\Exports\Models\Export;
useJavaabu\Exports\Policies\ExportPolicy;
useJavaabu\Permissions\Permissions;
publicfunctionboot(): void
{
// view_exports, edit_exports, delete_exports, force_delete_exports// with descriptions "View exports", "Edit exports", …Permissions::resource('exports', 'exports', label: 'Exports');
// extra permissions in the same group (name => English description)Permissions::register('exports', [
'download_exports' => 'Download exports',
]);
// the default policy; the app can override itPermissions::policy(Export::class, ExportPolicy::class);
}
ResourcePolicy maps the standard abilities to permissions:
Abilities
Permissions
viewAny, view
view_x
create, update
edit_x
delete
delete_x
restore, viewTrash
delete_x or force_delete_x
forceDelete
force_delete_x
Other abilities come from $abilities (a list of permissions means the user needs any of them); unknown abilities are denied. Optional properties:
$owned = true makes update and delete need edit_others_x / delete_others_x, or edit_x / delete_x plus $model->isOwnedBy($user).
$guard = 'web_admin' checks that guard's permissions.
Policy methods type-hint Illuminate\Database\Eloquent\Model $user, so any user model works.
In the app
php artisan permissions:sync # create/update permissions and roles (idempotent)
php artisan permissions:sync --prune # also delete permissions removed from a registered group
The sync is also available as Javaabu\Permissions\Database\Seeders\PermissionsSeeder, or Permissions::sync().
config/permissions.php (publish with --tag=permissions-config):
guards: the guards permissions are created for. When empty, the app's default guard is used.
roles: by default, super_admin with '*', which keeps it synced to every permission. Pattern lists such as ['view_*'] only ever add permissions.
policies: [Export::class => AppExportPolicy::class] overrides a package policy.
default_role, log_activity.
Policy precedence is: config('permissions.policies'), then the app's own Gate::policy() (in any provider), then the package default. Package policies are applied after the app has booted, so the app always wins.
More of the API
->guards(['web_admin']) limits a group to those guards.
->grantTo('editor', only: [...]) gives an existing role a group's permissions on sync.
->label('Exports') sets the group's label.
Permissions::role('auditor', 'Auditor', ['view_*'], guards: 'web_admin') defines a role in code.
Multi-auth helpers:
Permissions::guardFor($user)
Permissions::check(['view_exports'], 'web_admin')
$user->anyPermission([...], 'web_admin')
@anypermission([...], 'web_admin')
Role::forGuard(), Permission::forGuard() and Permission::inGroup()
Descriptions and labels are English strings used as translation keys. Ship their Dhivehi in your package's lang/dv.json (loadJsonTranslationsFrom). They're shown through $permission->translated_description and $permission->group_label.
Breaking changes from v2 that may affect you
No hard dependency on javaabu/helpers or javaabu/activitylog. Activity logging is optional, through spatie/laravel-activitylog.
Role no longer implements AdminModel.
Factories are now Javaabu\Permissions\Database\Factories\*.
Docs (docs/, llms.txt, Boost skill) are next. Controllers and routes for roles (#11) follow.
v3 is finished on next-gen, and CI is green on every job: PHP 8.4/8.5 × Laravel 12/13 × lowest/stable, Pint, React pages, and without translatable. The PR into main is #15, left for the user to review and merge.
Since the last update
Blade (javaabu/forms) and Inertia React (@javaabu/forms) views #10 role pages: in Blade (javaabu/forms v2) and Inertia React (@javaabu/forms, shipped through javaabu/manifest), with the same props in both. They have search, a card per permission group with select all and none, delete confirmation, and right-to-left layout. Apps can reuse RoleForm, PermissionGroupFields and RoleActions from @javaabu/permissions.
Optional javaabu/translatable support #12 translatable descriptions: role descriptions can be translated through Models\Translatable\Role and Support\Translations. A new migration adds nullable translations and lang columns to the roles table. Forms send one field per language (description_en, description_dv), and it's tested with and without translatable.
Activity
n4ff4h commentedon Nov 4, 2026
Implementation plan (v3.0)
All work is on the
next-genbranch (branch aliasdev-next-gen→3.x-dev); one PR intomainat the end.Architecture
Javaabu\Permissions\Permissionsbecomes a facade over a singletonPermissionRegistry. Packages call it from their service provider'sboot(); nothing touches the database until you sync.permissions.modelcolumn), each with a label, permissions (name => English description), optional guards and default role grants. It also holds the default role definitions and package policies.PermissionSyncer+php artisan permissions:sync(--prune,--guard=) +Javaabu\Permissions\Database\Seeders\PermissionsSeeder. It is idempotent: it creates or updates each permission for each guard, creates roles, and grants defaults. Grants only add permissions, except'*'roles, which are synced to everything.--pruneremoves registered-group permissions that are no longer registered.Permissions::policy(Model::class, Policy::class). These are applied to the Gate after boot, and only where no policy is already defined. The order is:config('permissions.policies')wins, then the app's ownGate::policy(), then the package default.ResourcePolicyis the base policy: it maps viewAny/view/create/update/delete/restore/forceDelete/viewTrash toview_x/edit_x/delete_x/force_delete_x, supports custom abilities through an$abilitiesmap, and supports optional ownership (edit_others_x).config('permissions.guards')(default: the app's default guard), unless a group names its own guards. Helpers:Permissions::guardFor($user),Permissions::check($permissions, $guard),$user->anyPermission($permissions, $guard),@anypermission($permissions, $guard),Role::forGuard(). NoApp\Models\Userorwebassumptions; events take anyModelas the causer.__()at display time, throughtranslated_descriptionandgroup_label), so a package ships them in itslang/dv.json.RolesController(web and JSON API), bound throughconfig('permissions.controllers'). Routes are registered byRoute::roles([...guard, prefix, middleware, name]).RolePolicyand therolespermissions are registered through the API above. Views render Blade or Inertia depending onconfig('permissions.views').Public API sketch
Order of work
llms.txt+ upgrading guide, Laravel Boost guideline and skill #8 Boost guideline and skill.Decisions and open questions
javaabu/activitylogandjavaabu/helpers. activitylog v2 (wave 4) will depend on permissions, and helpers requiresjavaabu/settings ^1.7, while settings v2 (wave 3) will also depend on permissions. Keeping either dependency would make apps on the next-gen stack unresolvable.spatie/laravel-activitylog.Roleno longer implementsAdminModel; it gets its ownsearchscope.docs/upgrading.md.RolekeepAdminModelthrough an optional subclass?Database\Factoriesnamespace toJavaabu\Permissions\Database\Factories, so they don't collide with the app's.HasRoles::updateRole()no longer callsget_setting('default_role'). It takes an explicit default role and causer.n4ff4h commentedon Nov 4, 2026
Permission and policy registration API is on
next-genThis is for every next-gen package with controllers. Require it like this:
The branch alias is
dev-next-gen→3.x-dev.Registering from a package
Do this in your service provider's
boot(). Nothing touches the database until the app runs the sync.ResourcePolicymaps the standard abilities to permissions:viewAny,viewview_xcreate,updateedit_xdeletedelete_xrestore,viewTrashdelete_xorforce_delete_xforceDeleteforce_delete_xOther abilities come from
$abilities(a list of permissions means the user needs any of them); unknown abilities are denied. Optional properties:$owned = truemakes update and delete neededit_others_x/delete_others_x, oredit_x/delete_xplus$model->isOwnedBy($user).$guard = 'web_admin'checks that guard's permissions.Policy methods type-hint
Illuminate\Database\Eloquent\Model $user, so any user model works.In the app
The sync is also available as
Javaabu\Permissions\Database\Seeders\PermissionsSeeder, orPermissions::sync().config/permissions.php(publish with--tag=permissions-config):guards: the guards permissions are created for. When empty, the app's default guard is used.roles: by default,super_adminwith'*', which keeps it synced to every permission. Pattern lists such as['view_*']only ever add permissions.policies:[Export::class => AppExportPolicy::class]overrides a package policy.default_role,log_activity.Policy precedence is:
config('permissions.policies'), then the app's ownGate::policy()(in any provider), then the package default. Package policies are applied after the app has booted, so the app always wins.More of the API
->guards(['web_admin'])limits a group to those guards.->grantTo('editor', only: [...])gives an existing role a group's permissions on sync.->label('Exports')sets the group's label.Permissions::role('auditor', 'Auditor', ['view_*'], guards: 'web_admin')defines a role in code.Permissions::guardFor($user)Permissions::check(['view_exports'], 'web_admin')$user->anyPermission([...], 'web_admin')@anypermission([...], 'web_admin')Role::forGuard(),Permission::forGuard()andPermission::inGroup()lang/dv.json(loadJsonTranslationsFrom). They're shown through$permission->translated_descriptionand$permission->group_label.Breaking changes from v2 that may affect you
javaabu/helpersorjavaabu/activitylog. Activity logging is optional, throughspatie/laravel-activitylog.Roleno longer implementsAdminModel.Javaabu\Permissions\Database\Factories\*.Docs (
docs/,llms.txt, Boost skill) are next. Controllers and routes for roles (#11) follow.n4ff4h commentedon Nov 4, 2026
Status: everything unblocked is done; views are blocked
On
next-gen(CI green on the full matrix):ResourcePolicyBlocked:
Needs the user:
CODECOV_TOKENisn't available to this repo, so coverage uploads are rejected (see Foundations: PHP 8.4 / Laravel 12+, CI matrix, coverage and codestyle #3).The
next-gen→mainPR will be opened when the epic is done.n4ff4h commentedon Nov 4, 2026
Final summary
v3 is finished on
next-gen, and CI is green on every job: PHP 8.4/8.5 × Laravel 12/13 × lowest/stable, Pint, React pages, and without translatable. The PR intomainis #15, left for the user to review and merge.Since the last update
RoleForm,PermissionGroupFieldsandRoleActionsfrom@javaabu/permissions.Models\Translatable\RoleandSupport\Translations. A new migration adds nullabletranslationsandlangcolumns to the roles table. Forms send one field per language (description_en,description_dv), and it's tested with and without translatable.llms.txt, Boost and the changelog are updated.For other packages
RoleResourcenow hascan,linksandpermissions_count. Role pages arepermissions::Roles/{Index,Create,Edit,Show}andpermissions::roles.*.Still open, needs a person
CODECOV_TOKENsecret.docs/advanced-usage/translations.md.