Impersonate a tenant for support work
Outcome
You see exactly what the customer sees, every action attributed to you in the audit log, with a banner reminding you the session is live.
Prerequisites
PLATFORM_ADMINorPLATFORM_SUPPORT.PLATFORM_READONLYis forbidden — a directPOST /platform/tenants/:id/impersonatereturns 403 and writesIMPERSONATION_FORBIDDENto the audit log.- Tenant status must not be
offboarding. Offboarding tenants reject impersonation to keep the export-only window clean. JWT_IMPERSONATION_EXPIRY(default 3600 s) governs how long the session lasts. There is no refresh token — when the JWT expires the banner redirects you back to the tenant list.
What an impersonation is — and isn't
| Is | Isn't |
|---|---|
A short-lived JWT with scope=impersonation, your platform_user_id, and the target tenant_id | A login as the tenant — there's no refresh token |
Recorded as IMPERSONATION_STARTED / IMPERSONATION_ENDED in identity.tenant_audit | A way to bypass PLATFORM_READONLY (forbidden, audited) |
Persisted in sessionStorage (cleared on tab close) | Persisted in localStorage (would survive tab close) |
| Subject to the audit middleware on every tenant API call | Bypass for suspended tenants — they still 503 |
Flow
Steps
Open the tenant page
Platform Admin → Tenants → <tenant>. Click the Impersonate button in the page header. The button is hidden forPLATFORM_READONLYand disabled when the tenant is offboarding.Confirm the consequences
The modal explains: every action you take is attributed to your platform user; the session ends on click or after the JWT TTL; closing the tab silently ends the session client-side (no
IMPERSONATION_ENDEDrow).Work in tenant context
After confirm, the page reloads to
/. TheImpersonationBannermounts at the top of the layout and counts down to expiry. Every API call you make during the session shows up inaudit.access_logwith your platform identity and inidentity.tenant_auditif it touches platform-managed surfaces.End the session
Click End impersonation in the banner. This posts to
/platform/impersonation/end, writesIMPERSONATION_ENDED, and redirects back to the tenant list.If you let the timer run out (or close the tab), the session ends but no
IMPERSONATION_ENDEDrow is written. That's expected — the JWT is server-side invalid, but the operator never explicitly closed the session.
Validation
| Check | Expected |
|---|---|
IMPERSONATION_STARTED row in identity.tenant_audit | details.email = your platform email |
IMPERSONATION_ENDED row | matches the START's platform_user_id (within JWT TTL) |
Platform Admin → Tenants → <tenant> → Impersonation tab | shows the session paired with startedAt + endedAt |
Tenant-side audit.access_log | rows during the window are attributed to your impersonation token |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Impersonate button hidden | You are PLATFORM_READONLY | Ask a PLATFORM_ADMIN to act on your behalf, or escalate the role temporarily. |
Click → 403 + IMPERSONATION_FORBIDDEN audit row | Tenant is offboarding, or RBAC drift | Check identity.tenant.status. If offboarding, the customer is mid-export — flip to active only with a written exception. |
| Banner shows but no tenant data renders | The tenant database is unreachable | See Tenant DB unreachable. |
Session ends without IMPERSONATION_ENDED row | JWT TTL elapsed or tab closed | Expected. To audit "abandoned" sessions per platform user, compare IMPERSONATION_STARTED count vs. IMPERSONATION_ENDED count over a window. |
History tab shows endedAt: null for an old session | The IMPERSONATION_ENDED row falls outside the page's limit | Re-query with a larger limit. Pairing logic matches each START with the next END for the same platform_user_id. |
Cross-references
- Tenant move + read-only gate — same audit
table records
TENANT_MOVE_*events. - Audit log viewer — the
global feed surfaces
IMPERSONATION_*andPLATFORM_PROVISION_REQUESTEDrows alongside tenant-scoped events.