CRM troubleshooting
A symptom → cause → fix table for the most common dev + ops issues. Bookmark this when something is "weird". See security.md for the broader gate model when 4xx/5xx looks wrong.
Authentication & permissions
| Symptom | Cause | Fix |
|---|---|---|
| 401 on every request | identity backend unreachable from CRM | curl ${IDENTITY_URL}/api/user -H "Authorization: Bearer $TOKEN" — should return 200 |
503 identity_unavailable | identity is up but slow / 5xx | wait — CRM auto-recovers within 60s (user cache TTL); SPA should NOT log out on this status |
403 tenant_not_a_member | token's tenants[] doesn't include the bound tenant | re-mint token; verify TENANT_ID matches one of the user's tenants in identity |
401 token_shape_legacy | token predates F-future-6 + 2026-06-15 cutoff hit | re-authenticate — old token shape no longer accepted |
403 service_account_scope_denied | service-account token's scopes[] doesn't cover the requested permission | add the scope at the identity-side API key; see service-accounts |
403 permission_denied | user's policies don't grant the verb | check identity-side role mapping; php artisan crm:list-permissions --fqn lists what CRM expects |
403 no_policies | identity returned an empty policies array | identity-side role missing? CheckPermission's policy fetch returned [] |
File upload / download
| Symptom | Cause | Fix |
|---|---|---|
403 File path rejected. on chat file serve | stored path contains .. (legacy row) or doesn't start with the channel prefix | DB hygiene — clean the metadata.path field |
422 version_chain_size_exceeded | document version chain hit 1 GiB total | delete older versions of that document |
| 422 mimetypes validation on media upload | client sent an HTML/SVG with a .png extension | use a real image; the whitelist is jpeg/png/gif/webp/avif/pdf |
Filename in DB has bin extension | original extension didn't match [a-z0-9]{1,8} regex | this is correct — the regex protects against path-traversal |
| Document update 403 even though user has folder write | source AND target folder write checked on move (KRITIKUS #4) | user must have write on both folders, not just the destination |
Data layer
| Symptom | Cause | Fix |
|---|---|---|
| ProjectMember re-add 422 "already a member" | soft-deleted tombstone exists | the add handler restores the tombstone; if it's claiming "active", check deleted_at in DB |
task.position collisions on the board | two concurrent creates raced before the lockForUpdate fix | re-run the create — DB::transaction + Project::lockForUpdate now serialises |
| Search returns nothing for "Árvai" | unaccent extension not loaded | CREATE EXTENSION IF NOT EXISTS unaccent (the migration does this; rerun migrate if you skipped it) |
| Search works for "arvai" but matches "Árva" too | working as designed — unaccent matches both | |
| SLA breach duplicate | concurrent CheckSlaBreachesJob runs raced past the partial unique index | check the partial unique index sla_breaches_unresolved_unique exists; rerun migration if missing |
Identity event consumer
| Symptom | Cause | Fix |
|---|---|---|
crm.identity-events.{tenant} queue not draining | worker isn't running | php artisan crm:consume-identity-events & |
TenantUser row missing for a user that logged in once | lazy-create not running, identity backend not emitting identity.tenant.member_added | check identity logs; the IdentityEventHandler is a logger-only on member_added — TenantUser is created by IdentityService::getUserFromToken on first call |
| Policy cache stale after identity-side role change | identity.policy.updated event not consumed (queue drained?) | check crm.identity-events.{tenant} is bound and consumed; on file/database cache driver, fall back is 1h TTL |
removed_at column reference in IdentityEventHandler | legacy code — already fixed (batch 1 menet 4) | pull latest |
Frontend
| Symptom | Cause | Fix |
|---|---|---|
Dashboard cURL error 7: localhost:8080 | DevOps API unreachable | DEVOPS_API_MOCK=true in main-backend/.env; restart make dev-admin-backend |
| Login redirects loop SPA → identity → SPA → … | OAuth client mismatch | sync OAUTH_CLIENT_ID + OAUTH_CLIENT_SECRET across identity seeder + main-backend .env |
Failed to resolve import @layouts/plugins/casl | Vuexy template ref to deleted plugin | shim already in place at src/@layouts/plugins/casl.ts |
:invalid_client 401 on token exchange | secret mismatch | make fresh + sync env (Passport seeder regenerates the secret each fresh) |
| Network 422 with no detailed error displayed | store didn't call notifyError(err, 'fallbackKey') | retrofit the store to use useApiError (see existing stores for pattern) |
| Pagination "page=3" in URL but list always opens on page 1 | usePaginationDeepLink not adopted on that page | apply the composable (see companies/contacts/projects/kb-articles refs) |
Identity ↔ CRM SDK drift
| Symptom | Cause | Fix |
|---|---|---|
| TS compile error in store after backend change | regenerated SDK missing locally | npm run api:sync |
CI fails on npm run api:check | generated SDK is stale | run npm run api:sync and commit the diff |
| Identity PolicySeeder rejects a CRM permission as unknown | identity-side PermissionRegistry doesn't have it yet | add to PermissionRegistry + reseed; crm:list-permissions --fqn --json is the canonical source |
Tests
| Symptom | Cause | Fix |
|---|---|---|
vitest command not found | dev deps not installed | npm i -D vitest @vitest/ui happy-dom @vue/test-utils |
| PHPUnit can't find Carbon::now() in unit test | unit tests don't bootstrap the Laravel container | use Feature TestCase if you need facades / DB; pure model tests work without it |
| Test passes locally but fails on CI | env-dependent (timezone, locale, DB driver) | wrap time-dependent assertions in Carbon::setTestNow(); tests that touch DB should declare RefreshDatabase |
Migration / fresh
| Symptom | Cause | Fix |
|---|---|---|
audit_logs has no column updated_at | seeder explicitly passed updated_at to a $timestamps = false model | already fixed (batch 1 follow-up) |
relation "project_members_project_id_user_id_unique" does not exist | partial unique index migration not run | php artisan migrate — covers it |
extension "unaccent" does not exist | superuser-only extension can't be loaded by the migration user | grant the migration role CREATEROLE + CREATEDB, or run CREATE EXTENSION unaccent as superuser before migrate |
Performance
| Symptom | Cause | Fix |
|---|---|---|
/api/user round-trip on every paginated XHR | post-2026-05-12 there's a 60s token-hash cache; if cold, expected | first request warms it; subsequent within TTL are cache hits |
| Policy fetch on every request | cache key includes a token-hash suffix (cross-repo #39); each new token mints a new slot | expected behavior — keeps stale-after-rotation impossible at cost of 1 fetch per token-rotation |
crm:list-permissions 30+ seconds | route count is large but no DB hit — should be sub-second | php artisan route:cache may have stale cache; clear with php artisan route:clear |
When all else fails
tail -f tenant/tenant-backend-crm/storage/logs/laravel.log— server-side errors- Browser devtools Network tab — request/response shapes
php artisan crm:list-permissions— what CRM expects to be in identity's PolicySeeder- RabbitMQ management UI — is the per-tenant queue bound + draining?
tenant/tenant-backend-crm/AUDIT_FINDINGS.md— has this case already been addressed?