nextdeveloper/iam
nextdeveloper/iam is a Laravel identity and access management package for managing users, roles, permissions, and authentication and authorization workflows. It aims to simplify access control setup, but has limited adoption and sparse documentation, so evaluate fit carefully.
Install and scaffold
composer require nextdeveloper/iam
php artisan vendor:publish --provider="Nextdeveloper\IAM\IAMServiceProvider"
php artisan migrate
Publish both the config and migrations in one pass if the package offers tagged publishing (--tag=iam-config, --tag=iam-migrations). Run migrations before you touch your users table so foreign keys resolve.
Where to look first
config/iam.php — the guard model, table names, and any super-admin/owner bypass toggle. This file tells you more about the package's opinions than the README does.User, which changes your whole design.src/Traits — this is the public surface you'll call from your app.Wire up your User model
use Nextdeveloper\IAM\Traits\HasRoles; // verify exact namespace in src/Traits
class User extends Authenticatable
{
use HasRoles;
}
First use case — protect an admin area
Seed two roles (admin, member) and a handful of permissions, assign admin to your own account in a seeder, then guard the route group:
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get('/admin', AdminDashboard::class);
});
That single flow — seed role → assign → middleware → verify a non-admin gets 403 — is the fastest way to confirm the package is installed correctly and to learn its naming conventions before you build anything else. Only after that baseline is green should you go near the v2.0.26 QR badge endpoints, since they're an additional privileged surface layered on the same authorization model.
Permission names as constants, not strings
Every IAM package degrades the moment permission strings drift between seeders, middleware, and Blade. Define them once:
enum Permission: string {
case InvoicesView = 'invoices.view';
case InvoicesEdit = 'invoices.edit';
}
Then reference Permission::InvoicesView->value in seeders and @can(...) in views. This costs ten minutes and saves you a week of 403s.
Seed idempotently
Use firstOrCreate / updateOrCreate for roles and permissions, never create. Role seeders get re-run during deploys and local resets; duplicates make assignRole behave unpredictably.
Separate the two authorization layers
role:admin). Cheap, readable, good for whole sections of an app.invoices.edit). Register policies the normal Laravel way and let them query the package:public function update(User $user, Invoice $invoice): bool
{
return $user->hasPermissionTo('invoices.edit')
&& $invoice->team_id === $user->current_team_id;
}
Don't cram tenancy scoping into role checks. Roles answer "what kind of user"; policies answer "may this user touch this record".
Blade and navigation
Gate UI on the same check the controller enforces, but derive nav items from permissions rather than hardcoding role names:
@can('invoices.view')
<a href="{{ route('invoices.index') }}">Invoices</a>
@endcan
If your sidebar is built from a config array, add a permission key per item and filter the array in a view composer. One place to change when permissions evolve.
Assign on the event, not in the controller
New users should get their baseline role from a Registered listener or an observer, not from RegisterController. Keeps registration, invite flows, SSO provisioning, and artisan-created users all consistent.
QR sign-in badges (v2.0.26+)
The new admin endpoints issue and revoke a per-user QR badge. Treat the badge as a credential, not a profile field:
role:admin — @can('iam.badges.issue') — so you can delegate badge printing to HR without handing over the whole admin area.Deactivated, Suspended, Deleted, and role-revocation events should all revoke outstanding badges. The one flow people forget is "user left the company, badge still opens the door."badge_id, issuer, timestamp, reason. If the package fires events on issue/revoke, subscribe; otherwise wrap the call in a small service that writes the audit row itself.Testing
Build a small helper so tests read clearly:
protected function actingAsAdmin(): static
{
return $this->actingAs(User::factory()->create())
->withRole('admin'); // or assign in the factory state
}
Add a factory state per role (->admin(), ->member()) and assert both the allow and the deny path. IAM bugs are almost always false-permits, and a suite that only tests happy paths won't catch them. With v2.0.26 in play, add three tests to the suite: non-admin cannot hit the issue endpoint, non-admin cannot hit the revoke endpoint, and a revoked badge no longer authenticates.
Admin surface
Resist building role management UI in v1. Ship an artisan command (iam:grant {email} {role}) plus a seeder, and defer CRUD screens until the permission set stabilizes. Role-editing UIs invite non-technical edits that silently break middleware assumptions. The same rule applies to badges: expose issue and revoke as two explicit buttons behind a permission before you build a badge management screen.
Wrap the new endpoints in a feature flag
Because v2.0.26 is a feature addition inside a patch-level release line, you may have upgraded without meaning to expose new routes. Guard your badge proxy behind a config flag (config('features.qr_badges')) so the surface can be turned off per environment while you finish the revocation wiring.
Cache invalidation is the #1 failure mode. Most packages of this shape cache a user's resolved permissions. After assigning or revoking a role in code, the change may not appear until the cache is flushed — and in long-running workers (Octane, queues) it may not appear until restart. Find the cache key or the forgetCachedPermissions()-style method in src/ and call it from your role-change service, not ad hoc from controllers. Wrap assignment in a single service class so the invalidation can never be forgotten.
Multiple guards. Sanctum, a separate admin guard, or API tokens each resolve permissions differently. Confirm in the config which guard the package inspects; if you have more than one, test explicitly that an API-token request sees the same permissions as a session request. This is where "works in the browser, 403 from the SPA" comes from — and it matters double for the v2.0.26 admin endpoints, which are API routes by nature.
The bundled admin endpoints are a route surface you didn't write. New in v2.0.26: issue/revoke badge endpoints. Verify (a) whether they auto-register on install, (b) their prefix, (c) what middleware they carry — auth alone is not auth + admin. If the package's own middleware assumptions don't match yours, don't patch vendor routes: disable route registration in config if supported, and re-expose the logic through a controller you own.
Revocation lag on badges. A revoked QR badge is a cache entry in someone else's system as much as yours. Check whether revoke is immediate (DB row deleted) or soft (flag flipped, checked at scan time). If it's soft, confirm the scanning path actually reads the flag; if it's cached anywhere, flush it in the same service call that revokes. Also confirm revocation is idempotent — offboarding hooks fire twice more often than you'd expect.
Super-admin bypass is a footgun. If there's an owner/super-admin short-circuit in a Gate before() callback, it will silently make every @can pass for that user — including in tests you wrote to verify denials. Know whether it's on, whether it respects Gate::forUser(), and whether it also short-circuits the new badge permissions. A super-admin who can silently mint badges is a compliance finding.
Permission naming collisions with Laravel. If the package registers a role middleware alias or a can-adjacent helper, check bootstrap/app.php/Kernel.php for conflicts with your own aliases. Alias collisions fail loudly at boot; helper function collisions fail silently at runtime.
Migration ordering with an existing user base. On a live app, adding role tables after users exist means you need a backfill migration or seeder that grants a default role to every existing row. Without it, day-one deploy locks everyone out. Ship the backfill in the same deploy as the middleware.
Version pinning and abstraction. This is a single-maintainer package with a very small install base and no downstream dependents — treat it as a vendored dependency, not infrastructure. Pin an exact version in composer.json, and put your own thin interface in front of it:
interface AccessControl {
public function assign(string $role, User $user): void;
public function allows(User $user, string $permission): bool;
public function issueBadge(User $user): string;
public function revokeBadge(User $user): void;
}
One adapter class implementing that interface means swapping to spatie/laravel-permission later is a day's work instead of a refactor across every controller, policy, and Blade file. The two badge methods are what stop the v2.0.26 endpoints from leaking into forty call sites — and if badge issuance later moves to a dedicated package or an internal service, you change one file. Given the package's stated philosophy — "make it as complicated as they want" — you will outgrow the defaults, and the abstraction is what lets you do so incrementally.
Check the release metadata. The listed last release date is in the future relative to normal release cadences, which usually signals a version-tagging quirk or a mirrored/automated publish. v2.0.26 shipping a user-facing feature under a patch version is another signal that the project's semver discipline is loose: a require: "nextdeveloper/iam": "^2.0" constraint can hand you new HTTP endpoints on a routine composer update. Pin exact versions, read the diff before upgrading, and check whether new routes were registered after every bump.
Extension points worth knowing. The config file and the publishable migrations are your sanctioned customization surface — add columns there rather than editing vendor files. If the package fires events on role assignment/revocation — and now on badge issue/revoke — subscribe to them for audit logging; that's cheaper than wrapping every call site and gives you a compliance trail for free.
How can I help you explore Laravel packages today?