Impersonation session isolation means a support agent’s customer session stays where you put it: in one tab or one browser profile, always with a banner and the right restrictions, and it ends without leaving the agent signed in as the customer. Most apps don’t get this for free. Their login is a cookie, cookies are shared by every tab in a browser profile, and so a reload or a new tab quietly acts as the customer too.
The fix has three parts: keep staff and customer sessions in different places, make every tab prove it belongs to the support session before it renders, and end everything from the server.
Why does impersonation leak into other tabs?
Because different browser storage has different scope. Your auth cookie is shared across tabs, while anything you keep in a page’s memory or in sessionStorage is not.
MDN describes sessionStorage (opens in a new tab) as partitioned by origin and by tab, and notes that opening a page in a new tab creates a new session, “which differs from how session cookies work.”
| Where state lives | Shared with | What that means for impersonation |
|---|---|---|
| Cookie on your app’s host | Every tab and window in the profile | Reloads and new tabs are signed in as the customer |
| localStorage | Every tab on the origin, until cleared | Same as cookies, and it survives restarts |
| sessionStorage | One tab, but copied to a page opened from it | Mostly per tab; not a secret boundary |
| In-memory JavaScript state | One page load | Lost on reload; must be re-established |
So the usual failure looks like this. Support opens the customer’s account in tab A, and a banner shows “Viewing as Amber, read-only”. They middle-click a link, and tab B loads the same app with the customer’s cookie but none of tab A’s in-memory state. Tab B has no banner, and any browser-side restrictions that lived in tab A’s memory aren’t there either.
What happens to the staff member’s own session?
It’s usually replaced in that browser, because one profile holds one login per site.
Clerk. Clerk’s user impersonation guide (opens in a new tab) says an actor token’s URL is a Clerk Frontend API URL that will “sign out existing users” and prepare sign-in for impersonation. If an agent who also uses your product as themselves opens it in their normal profile, they’re signed out of their own account.
Better Auth. The Better Auth admin plugin (opens in a new tab) creates a session that mimics the user. It stays active until the browser session ends or it reaches one hour by default, and stopImpersonating returns to the admin account. That restore only happens in the browser that runs it.
For agents who use your product as themselves, the simplest isolation is a separate browser profile just for support work. Chrome profiles keep browsing data such as history, passwords and settings separate, and Google’s Chrome help suggests (opens in a new tab) them for keeping different accounts, like work and personal, separate. A distinct profile color and name also make it obvious which window is acting as a customer.
Which impersonation session isolation option fits your team?
Pick based on how often support impersonates and whether staff also use the product as customers do.
| Option | How it isolates | Choose it when | Watch for |
|---|---|---|---|
| Separate browser profile | Separate cookie jar and storage per profile | Staff use the product as themselves, or you want a quick operational fix | It’s a habit, not a control; nothing stops an agent using the wrong profile |
| Per-tab capability with server restore | Each tab must re-establish the support context before it renders | You want reloads and new tabs to behave predictably | Needs backend and frontend work |
| Separate support origin | Host-only cookies stay on that host | You can serve the app on a dedicated hostname | Auth providers and redirects must support that host |
| Short-lived credential and server revocation | Limits how long any leaked tab stays useful | Always | Doesn’t fix the missing banner on its own |
The separate-origin option relies on host-only cookies. According to MDN’s Set-Cookie reference (opens in a new tab), a cookie set without a Domain attribute is only returned to the host that set it, not to subdomains. That can keep a support session off your main app host, but it’s a bigger change than most teams need.
These options stack. A separate profile is cheap and helps humans. Per-tab restore and server revocation are what make the system behave correctly when a human slips.
How do you keep per-tab state honest?
Treat each tab as unknown until it proves it belongs to the support session. Keep the capability out of persistent storage, have each new page load ask your server whether the logged-in session is impersonated, and render nothing protected until the answer is in.
A few rules make that work:
- Keep the capability in memory. It disappears on reload, which is the point. Re-establish it through an authenticated call to your backend, which already knows from verified session state whether this login is an impersonation.
- Don’t treat sessionStorage as a secret. A page with an opener starts with a copy of the opener’s sessionStorage, as MDN’s sessionStorage page (opens in a new tab) notes. It’s fine for a non-secret tab ID, not for a credential.
- Fail closed in the browser. If a tab knows it’s impersonated but can’t restore the support context, show a safety screen, not the customer’s data without a banner.
- Tell sibling tabs about lifecycle events.
BroadcastChannellets same-origin tabs, windows and frames message each other, per MDN’s BroadcastChannel docs (opens in a new tab). Use it so an end in one tab is reflected in the others quickly, and never send tokens or customer identity over it. - Keep the server authoritative. Browser coordination is a convenience. The backend still has to reject expired or revoked sessions on every request.
The banner deserves its own rule: every tab that shows customer data during a support session should show it. If a tab can’t show the banner, it shouldn’t show the data either.
Side effects follow the same logic. If your analytics suppression reads only browser state, an unrestored tab will track the customer as usual. Deriving the flag from the server session avoids that; see user impersonation side effects.
How does Devora handle new tabs and reloads?
A Devora session starts in the tab that opened the Devora link, and Devora’s per-tab capability is never written to browser storage. As Devora’s frontend integration guide (opens in a new tab) explains, your own login is shared with other tabs, so without a session bridge a reload or new tab runs as the impersonated user with no banner, scope enforcement or capture.
The session bridge closes that gap:
- You pass a
sessionBridgeoption to the frontend SDK. It calls an authenticated route on your backend, which you mount with the backend SDK’s browser-session helper. - That route, described in the Node SDK bridge reference (opens in a new tab), returns
nonefor ordinary users without contacting Devora. For an impersonated session it returns a 30-second, single-use resume code bound to that tab and origin, orblockedwhen Devora can’t safely restore it. - The tab redeems the code for its own capability, then starts scope enforcement and capture. A restored tab doesn’t call
onImpersonateagain, per the JavaScript SDK reference (opens in a new tab).
Until the bridge answers, and whenever it reports blocked, protected content must not render. The React and Solid providers handle this for you; in Vue and Svelte you wrap protected content in BridgeGuard. Sibling tabs are told over BroadcastChannel when a session ends, and Devora’s validation check remains authoritative, as the JS reference on restoring tabs (opens in a new tab) describes.
One clarification on “no scope enforcement”. That refers to Devora’s browser-side layers. The backend guard is the security boundary (opens in a new tab), and it still applies to the routes it protects, as long as your server session carries the verified Devora context. The bridge is what keeps the banner, browser guards and capture consistent across tabs.
How should an impersonation session end cleanly?
End it on the server first, then clean up every tab. Browser code only runs in tabs that are still open, so it can’t be the whole mechanism.
In Devora, onSessionEnd runs when the agent ends the session, the time limit is reached, or the session is ended in Devora or another tab. It should clear the impersonated auth state and send the browser to a safe signed-out or staff route. Devora’s frontend guidance is explicit (opens in a new tab): don’t silently fall back to the previously signed-in customer, and if your provider supports restoring the staff session, follow its documented flow.
The server side is a signed termination request (opens in a new tab) to your backend for every ending, retried if delivery fails and sent even with no tab open. Your handler revokes the credential by Devora session ID. The session termination guide covers revocation in detail, and the Clerk impersonation guide covers revoking a Clerk session.
Test isolation in staging
Use a staging customer, a staff account that also uses the product, and two browser profiles. These are pass conditions to check, not results we’ve run.
| Scenario | Expected outcome |
|---|---|
| Open the impersonation link in the support profile | Staff member’s main-profile session is unaffected |
| Reload the impersonated tab | Banner and scope return, or a safety screen shows; never customer data without a banner |
| Open a link from the impersonated tab in a new tab | Same as reload |
| Duplicate the impersonated tab | New tab gets its own capability or is blocked |
| Bridge route returns an error | Protected content doesn’t render |
| End the session in one tab | Other tabs leave customer data and reach a safe page |
| End the session from the Devora dashboard with the tab in the background | Next request from that tab is rejected by the backend |
| Agent returns to their own account | Signed in as staff, not as the customer |
If you want per-tab restore, the banner and server-side endings without building them, create a Devora account and try one staging session against this table.
Sources
- MDN: Window.sessionStorage (opens in a new tab)
- Clerk: User impersonation (opens in a new tab)
- Better Auth: Admin plugin (opens in a new tab)
- Google Chrome Help: Manage Chrome with multiple profiles (opens in a new tab)
- MDN: Set-Cookie header (opens in a new tab)
- MDN: BroadcastChannel (opens in a new tab)
- Devora: Frontend integration (opens in a new tab)
- Devora: Node SDK reference, browser-session bridge (opens in a new tab)
- Devora: JavaScript reference, restoring sessions in other tabs (opens in a new tab)
- Devora: Scope and security (opens in a new tab)
- Devora: Session lifecycle (opens in a new tab)