Impersonation β
Audience
This article is for GCM platform operators. Church admins should see Users & Roles β impersonation is only available to staff with the platform_admin or superadmin claim, and is the single most audited action in the platform.
Impersonation lets a staff user enter a customer's workspace as if they were that org's admin, so they can reproduce a bug, run a one-off fix, or walk a customer through a screen on a call. It is a regular feature, not a back door β every entry and exit is captured in platform_audit_log, scoped to a sessionStorage key (not localStorage, not a cookie), and ends automatically when the tab closes.

How to start a session β
From the Organisations list, open any row's detail sheet (the chevron at the end of the row, or Open from the Billing ops table). The right-side sheet shows plan, status, staff, and modules. The blue Open account button at the top of the actions row is the entry point.
Click it, and three things happen in order:
- A confirmation dialog asks for a free-text reason. This is required.
- The frontend calls the
start_impersonationPostgres RPC with the target org id and the reason. - If the RPC succeeds, the row in
admin_impersonationis created, the org id is written to sessionStorage under the keygcm_impersonation, the React Query cache is cleared (to prevent cross-org data leakage), and the user is redirected to/β which now resolves to the customer's tenant shell.
A red banner reading "Viewing as ... (impersonation)" appears across the top of the tenant shell. The customer's admin pages render as if you were them: their dashboard, their members, their giving. You can click any button their admin could click.
One target at a time
Starting a new impersonation while one is in progress is blocked by the RPC. End the current session first. The behaviour is intentional β the audit table assumes one active row per actor.
What the reason is for β
The reason field is mandatory and ends up in three places:
- The
admin_impersonation.reasoncolumn. - The
platform_audit_logrow withaction = 'impersonation_started'. - The 7-day "Impersonations" KPI on the audit-log tab.
Write something a future auditor can read. "Support ticket #4421 β donor sees a 500 on giving form" is good. "checking" is not. The auditor is you, in 90 days, after the customer complains.
How RLS keeps you scoped β
While you are impersonating, the current_org_id() SQL helper returns the target org id rather than your own. Every RLS policy on the platform uses current_org_id() (not raw JWT claims), so reads and writes are silently scoped to the customer's data. You can read their members because the policy passes; you cannot read another org's members because the helper does not return their id.
This is why we use current_org_id() and not auth.jwt() ->> 'organization_id' in policies. The latter would leak through impersonation.
There is one extra safety net: edge functions read the impersonation context through getCallerContext so they too know they are running under a customer's id, not yours. Audit logs include both actor_id (you) and target_org_id (the customer) so a forensic trail exists either way.
How a session ends β
A session ends in three ways:
- You return to platform mode β click Return to platform in the banner. The frontend calls
end_impersonation, clears sessionStorage, clears the React Query cache, writes animpersonation_endedaudit row, and pushes you to/platform. - You close the tab β sessionStorage is wiped automatically. The
admin_impersonationrow stays open until you log in again, but the JWT scope is gone and the staff user's next login closes it. Reason this design exists: a stolen device cannot resume a session by reopening the tab. - You sign out β same as closing the tab from the impersonation point of view.
Compounding (impersonating, refreshing the tab, impersonating someone else) is blocked because the new RPC call ends the prior row before creating a new one.
What you should and should not do β
Do
- Reproduce the bug the customer reported, then return.
- Walk a customer through a screen on a call β they see your cursor, you see their data.
- Run a documented one-off fix (e.g. seed a missing default fund) when they have approved the change in writing.
Don't
- Edit configuration the customer would not expect you to touch. If you are not sure, ask first and link the request in the reason field.
- Send messages, post to channels, or trigger workflows as the customer. The recipient sees the org's name and number, not yours β anything you send is permanently attributed to them.
- Stay in impersonation longer than the task takes. The banner is visible to anyone who walks past your monitor.
Reading the impersonation audit trail β
The Audit log tab carries a 7-day KPI for impersonations and the AuditLogViewer lets you filter by action. Three actions matter:
impersonation_startedβ successful start. Metadata includestarget_org_id,org_name,reason.impersonation_endedβ clean exit via Return to platform.impersonation_failedβ thestart_impersonationRPC rejected the call. The metadata includes the underlying error. A failed attempt is usually an RLS misfire, a target org that no longer exists, or a staff account that just lost its claim.
Filter by actor_email = <you> to see your own sessions over the last 7 days. Filter by metadata->>'target_org_id' = <id> to see every staff session against one customer β a useful answer to "has anyone from your team logged into our account recently?".
When something feels wrong β
If you arrive on a tenant page and the data does not look like the customer's β wrong member names, wrong language, wrong org logo β return to platform mode immediately. Two failure modes can cause this:
- Stale cache β the React Query cache survived the entry. Hard-refresh the page; if that fixes it, the bug is in the entry path and worth a Sentry issue.
- Stuck impersonation row β your previous session was not closed cleanly and
current_org_id()is returning the old target. Sign out fully, sign back in, and the auth-hook will reset the JWT.
In both cases, write an audit entry afterwards explaining what you saw. The whole point of the impersonation surface being auditable is that we learn from edge cases.
