Migrating from spatie/permission
The drop-in shape
iam.can: is intentionally shaped like Spatie’s permission: middleware, so the route diff is minimal:
// before — spatie/laravel-permission
Route::get('/reports', ReportController::class)
->middleware('permission:reports.view');
// after — laravel-iam-client
Route::get('/reports', ReportController::class)
->middleware('iam.can:reports:view');
The shape is the same; what changes is where the rule lives. With Spatie, reports.view is a row in your
app’s permission table. With IAM, reports:view is declared in the app’s manifest on the server, and decided
by the central PDP — with ABAC conditions, ReBAC scoping and step-up that a flat permission table can’t
express.
What you gain
| spatie/permission | laravel-iam-client | |
|---|---|---|
| Where rules live | this app’s DB | central PDP (one place for all apps) |
| Model | RBAC | RBAC + ABAC + ReBAC |
| Per-resource checks | manual | iam.can:perm,routeParam (route-bound) |
| Assurance / step-up | none | requiresStepUp honored by granted() |
| Failure mode | local query | fail-closed transport |
| Audit | per-app | tamper-evident, centrally |
Use the dedicated bridge for a safe cutover
A like-for-like middleware swap is the end of a migration, not the whole of it. To get there safely — to
prove the PDP returns the same answers as your Spatie tables before you enforce them — use the dedicated
migration package:
laravel-iam-bridge-spatie-permission
scans your existing roles/permissions, generates a manifest, runs a shadow mode that diffs Spatie’s
decisions against the PDP’s without enforcing, then supports cutover and rollback. It’s the recommended path
for anything beyond a trivial app.
Why gate.enabled = false during shadow mode
Shadow mode works by comparing two answers for the same check: what Spatie would decide vs what the PDP
decides. If the IAM Gate adapter is also enforcing during that window, it changes the observed outcome and
corrupts the diff.
// config/iam-client.php — while the bridge runs in shadow mode
'gate' => [
'enabled' => false, // observe & diff only; don't let the adapter enforce
],
Once the diffs are clean, flip gate.enabled back to true and let the adapter enforce — that’s the cutover.
Suggested sequence
- Install the bridge and generate a manifest from your Spatie data.
- Shadow mode with
iam-client.gate.enabled = false— collect and resolve decision diffs. - Cut over abilities by namespacing them and re-enabling the Gate adapter (
gate.enabled = true,
intercept = namespaced). See Coexistence. - Swap middleware
permission:→iam.can:on routes as you go. - Decommission the Spatie tables once every ability is centralized and verified.
Gotchas
Spatie commonly uses dotted keys (reports.view); IAM uses namespaced colon keys (reports:view). Decide on
the canonical PDP key format and map consistently — the bridge helps, but verify the keys your routes pass
match the manifest.