Every multi-tenant SaaS eventually ships the same request: support needs to see what the customer sees. The account is in a bad state, the screenshot in the ticket does not match the API response, and nobody can reproduce it from the outside. Someone proposes a "Login as customer" button. It ships in an afternoon. It is also the single most dangerous endpoint in the product.
Impersonation collapses the boundary your entire permission model is built on. The person clicking the button is staff; the session they receive is the tenant's. If that transition is not written down with enough fidelity to reconstruct it months later, you do not have an audit trail — you have a demo.
This is the design we build into multi-tenant systems that hold other people's data, and the failure modes we look for when reviewing an existing one.
The three questions an impersonation log must answer
Most teams instrument impersonation as a single event: admin.impersonation.start. That line answers "did someone impersonate somebody" and nothing else. Production incidents need more.
- Who was the human? Not the service account, not the shared support login. The individual employee, tied to an identity provider assertion with a signed subject claim.
- What did they actually do while inside? Impersonation without action logging is a read receipt. If the impersonated session touched approvals, billing, or configuration, those writes must be attributable to the impersonator as well as the tenant.a
- When did it end, and how? A session that expires silently is indistinguishable from one that was abandoned mid-action. Both need an explicit terminal record.
If your current schema cannot answer all three from a single query, the audit trail is decorative.
Model the transition, not just the session
A session table with an impersonated_by column is a start, but it puts the audit story inside the same row that authorization reads from. That coupling is how people lose evidence: rows get updated, sessions get rotated, and the original actor reference quietly changes.
Prefer an append-only event stream next to the mutable session record.
create table impersonation_events (
id bigserial primary key,
tenant_id uuid not null,
impersonator_id uuid not null, -- staff user
impersonated_id uuid not null, -- tenant user
session_id uuid not null,
ticket_ref text not null, -- why this was allowed
event_type text not null, -- requested | approved | started | ended | denied
occurred_at timestamptz not null default now(),
request_ip inet,
user_agent text
);
revoke update, delete on impersonation_events from app_rw;
Two details matter more than the column list. The table is append-only at the grant level, so application bugs cannot rewrite history. And the ticket reference is mandatory: an impersonation request without a stated reason is an incident waiting to be discovered late.
If you already run transactional outbox patterns for other durable side effects, impersonation events belong on the same rail — see Postgres outbox workers that skip locked rows for the mechanics of writing audit rows and downstream copies atomically.
Separate the authorization decision from the assumption
"Login as customer" bundles two decisions that should be independently gated:
- Authorization: may this employee impersonate any tenant at all, and which tenants?
- Assumption: may this employee take over this session right now, for a stated reason, with a defined expiry?
The first is a role. The second is a time-boxed grant with a business justification, and it should be reviewable after the fact.
A workable shape:
| Control | Wrong default | Defensible default | | --- | --- | --- | | Duration | Session lasts until logout | 30–60 min hard expiry, no renewal without a new approval | | Scope | Full tenant access | Read-only unless write scope explicitly requested | | Reason | Optional free text | Required ticket reference, validated against a real ticket | | Notification | None | Tenant admin notified at start and end | | Reversibility | Silent | Visible "support session active" banner in the tenant UI |
That last row is not decoration. Tenant-visible banners convert a covert capability into a disclosed one, and they change how your own staff behave while inside.
Writes made under impersonation need two owners
This is where most implementations quietly break. A support engineer impersonating a tenant updates a payment method. The write is authorized as the tenant because the session belongs to the tenant. Six weeks later the customer disputes the change and your records say the customer did it.
Every write performed inside an impersonated session should carry both actors:
{
"tenant_id": "...",
"actor_user_id": "...",
"on_behalf_of_user_id": "...",
"impersonation_session_id": "...",
"action": "billing.payment_method.update",
"before": { "brand": "visa", "last4": "4242" },
"after": { "brand": "visa", "last4": "1881" }
}
Without the on-behalf-of actor, tenant-facing activity feeds become misleading, and support cannot answer "who changed this" without joining three tables that were never designed to meet. Retention and immutability rules for that record are their own problem — the tradeoffs are covered in how long to keep SaaS audit logs.
The failure modes worth testing before you ship
Run these against staging with two tenants and a staff account:
- Cross-tenant escape. Start impersonating tenant A, then attempt to read a tenant B record by ID. The impersonated session must inherit tenant A's row-level policy exactly — no widened scope, no admin bypass flag left enabled.
- Privilege carry-over. Impersonate a tenant admin and confirm the resulting session cannot reach platform-level endpoints. Impersonation should narrow authority, never escalate it.
- Orphaned sessions. Kill the staff user's SSO session mid-impersonation. The impersonated session must terminate, not continue on a stale token.
- Missing evidence. Delete the ticket referenced by an active impersonation. The session should end or flag itself; a live grant pointing at a dead justification is unattributable access.
- Replay of the approval. Reuse an approval token for a second session. It must fail — impersonation grants are single-use, and idempotency keys on the grant endpoint should not resurrect them. The same reasoning about retried side effects applies to preventing duplicate actions when payment workflows retry.
Recovery paths are part of the threat model
Impersonation is often justified by recovery: the customer lost their second factor, the workspace owner left, nobody can reach billing. That is a legitimate need, and it is also the exact scenario an attacker will claim.
Keep the recovery path explicit and separate:
- Impersonation for diagnosis is read-mostly and short.
- Impersonation for recovery requires a second approver and a customer-visible confirmation.
- Account-takeover recovery on the tenant's own credentials should follow a documented flow such as passkey and account recovery for SaaS rather than being silently solved by staff impersonation.
Two staff members and a logged approval are cheap. Explaining to a customer why an employee read their data without a record is not.
What to instrument for the people who review it
Your security reviewers, your customer's procurement team, and eventually a regulator will ask for the same three reports. Build them before you are asked:
- By employee: every impersonation session in a date range, with ticket references and durations.
- By tenant: who entered this workspace and when, including failed attempts.
- By action: every write performed on behalf of a user, with before/after state.
If any of those require a data engineer and a weekend, the audit trail is not operational. A support tool is only trustworthy when the log of its use is as easy to read as the screen it opens.
The build order that avoids rework
If you are adding impersonation to an existing multi-tenant product, sequence it this way:
- Append-only event table with mandatory ticket reference and explicit started/ended events.
- Tenant-visible banner and admin notification on session start.
- Dual-actor attribution on every write inside the impersonated session.
- Hard expiry, single-use grants, and termination on staff session loss.
- The three review reports, generated from the event stream rather than the live session table.
Steps one through three are schema work and they are cheap before launch. After launch they are migrations against tables that already contain other people's data — the slowest, most expensive version of the same task.
If you are wiring this into a product that already ships tenant isolation, our team builds the custom AI and platform software behind these patterns and can review an existing impersonation path in place. For the broader integration picture, see AI integration work. When you want a second pair of eyes on your current audit design, send us the details.