Troubleshooting
Troubleshooting
Symptom → likely cause → fix. Most issues come from configuration cache, a missing Spatie trait, or a
mismatch between the application prefix and the registered manifest.
No mismatches are ever logged
Most often: the observer isn’t registered, or there genuinely are none
modeisn’tshadow.ShadowGateis registered only whenIAM_SPATIE_MODE=shadow. Check the
resolved value (php artisan tinker→config('iam-spatie.mode')) andconfig:clearif you cache config.- The log channel is silent. Verify
IAM_SPATIE_MISMATCH_CHANNELpoints at a channel defined in
config/logging.phpatlevel: warningor lower. - No
Gatechecks are happening on the path you’re testing.ShadowGateonly fires onGate/can()
checks. Code paths that don’t authorize produce no records. - They genuinely agree. A clean diff is the goal — confirm with a deliberately divergent test (grant a
permission in Spatie that IAM lacks) to prove the pipe works.
Everything mismatches
applicationprefix ≠ registered app key. IfIAM_SPATIE_APP=billingbut the manifest was registered
underlegacy, everyfull_key(billing:...) is unknown to IAM → all deny → mass
spatie_allow_iam_deny. AlignIAM_SPATIE_APPwith the registeredapp.key.- Manifest not registered (or not yet propagated). Run
iam:app:registerand confirm the app exists on
the server before reading the diff. - Slug drift. A permission renamed in Spatie after registration slugs to a key IAM doesn’t have. Re-run
iam:spatie:manifest, re-register.
A suspiciously clean diff (possible false-zero)
Zero mismatches but you expected some?
If another Gate::before short-circuits the gate (e.g. the IAM client already enforcing on a partially
migrated app), the comparison could be measuring IAM against IAM.
- Confirm the Spatie trait is present.
spatieAllows()probeshasPermissionTo; if the user model lacks
Spatie’sHasRoles/HasPermissionstraits the probe falls back to the gate result — exactly the
false-zero risk. Ensure the migrated model uses the Spatie trait. - Inject a known divergence and verify it is logged. If it isn’t, the probe is falling back — fix the
model traits.
The scanner finds nothing / wrong tables
- Custom Spatie table names.
SpatieScannerreadspermission.table_names. If your Spatie tables are
renamed, confirm that config is correct — the scanner honors it via the service provider’stableNames(). - Wrong database connection. The scanner uses the default connection
(ConnectionResolverInterface::connection()). If Spatie lives on another connection, point the default
connection (or the Spatie config) appropriately for the scan. - Empty result. A scan of an empty Spatie install yields empty arrays —
report.mdwill show zero roles
and permissions. That’s correct, not a bug.
Mode change seems ignored
- Config cache. After editing
IAM_SPATIE_MODE, runphp artisan config:clear(or re-cache). The mode
is read at boot from cached config if present. - Wrong
.envfor the environment. Per-app deployments each have their own env — make sure you changed
the one the running app reads.
Shadow adds latency
- One
IamClient::can()perGatecheck. Shadow doubles authorization work by design. Keep the IAM
client’s policy cache warm (see the
client docs); shadow is a temporary phase, not the steady
state. - Scope the shadow window. You don’t need shadow on forever — run it long enough to cover representative
traffic, then cut over.
When in doubt, prove the pipe
The fastest way to diagnose shadow is a controlled divergence: grant a permission in Spatie that IAM lacks
(or vice versa) and confirm exactly one iam.shadow.mismatch with the expected direction. If that works,
the machinery is sound and the issue is in your data/mapping.
Next
- Configuration — the keys behind these symptoms.
- Decision diffing — why the false-zero happens and how the probe avoids it.