Authentication failure
Symptoms
401 or 403 on a route that worked in another tenant. auth.error.staleSession. Refresh fails. AI routes return ai.error.workspaceDisabled.
Likely causes
| Symptom | Cause in code |
|---|---|
| 401 on a normal route | Missing access JWT, or not @PublicRoute |
auth.error.staleSession | JWT tenantId no longer exists |
| 403 on a permissioned route | @RequirePermissions and the membership does not have every listed permission |
| Call succeeds without the permission you expected | The handler has no @RequirePermissions. The guard allows it |
| Refresh 401 | Refresh route is public but JwtRefreshGuard still requires the refresh token |
| Warehouse 403 | x-warehouse-id is outside the membership’s warehouses |
AI 403 ai.error.workspaceDisabled | AI_WORKSPACE_ENABLED is not the string true |
| Login 400 with Turnstile | Site and secret keys are set and the token is missing. Blank keys disable the check |
How to investigate
- Decode the access token and compare
tenantIdwithGET /v1/auth/my-tenants. - Read the controller decorators. Do not infer a deny from the module name.
- Permission cache is 60 seconds. A role change can lag.
Passwords
Unknown: which flows hash with argon2 and which use bcryptjs. Both libraries are dependencies.
Impersonation
POST /v1/auth/impersonate issues a 30-minute token. The controller summary says Admin only. The exact @Roles or permission decorator was not re-read for this page. Check the handler before you assume a platform role.