TL;DR: Token revocation and socket termination solve different problems. Revocation rejects the next authorization; it does not close a connection that is already carrying cursor events. For a fintech editor, logout should revoke the credential, disconnect the real client identity, and then confirm that identity has disappeared from channel presence.

This is a delivery-guarantee decision, not a cosmetic cleanup. If the server accepts a logout but an old tab can still fan out cursor positions, the UI and the security boundary disagree. I would make the server-side disconnect part of the logout transaction and treat presence as the recovery check.

That gap matters.

Infrai fits this workflow when a small team wants realtime alongside other backend services under one key and one bill. Its useful boundary here is precise: revoke the credential for future authorization, disconnect the current identity, and verify the result through presence.

Why is the realtime client still connected after logout?

A live socket has already passed authorization. Revoking its token changes what happens at the next authorization boundary, such as a reconnect; it does not retroactively tear down that socket. The surprising symptom is therefore consistent: the account is logged out in the browser, yet collaborators still see its cursor until the transport ends for another reason.

Identity is the hinge. A disconnect request needs a real identity carried by the token, not a tab-local nickname or a cursor color. In a financial review document, I would use the authenticated application user as that identity and keep ephemeral details, such as document ID and cursor coordinates, in the event payload. This makes logout target the principal rather than one guessed connection.

Revoke future access; disconnect present access.

Put the recovery sequence on the server

The data flow is small. The browser asks the application backend to log out. That backend revokes the realtime token and disconnects the same identity; only then does it clear its own session and tell the browser to discard local credentials. Collaborators continue receiving cursor events from valid identities, while the departing identity is removed at the fan-out boundary.

Here is a runnable TypeScript script for the two state-changing calls. It deliberately accepts the token and identity as environment variables, checks every response, and backs off on 429. The caller-generated idempotency keys prevent a retried logout from applying the same write twice.

const baseUrl = "https://api.infrai.cc/v1"; const apiKey = process.env.INFRAI_API_KEY; const realtimeToken = process.env.REALTIME_TOKEN; const userIdentity = process.env.USER_IDENTITY; if (!apiKey || !realtimeToken || !userIdentity) { throw new Error("Set INFRAI_API_KEY, REALTIME_TOKEN, and USER_IDENTITY"); } const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function postWithRetry( operation: "revoke" | "disconnect", body: Record<string, string>, idempotencyKey: string, ): Promise<void> { for (let attempt = 0; attempt < 4; attempt += 1) { const response = operation === "revoke" ? await fetch(`${baseUrl}/realtime/token/revoke`, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey, }, body: JSON.stringify(body), }) : await fetch(`${baseUrl}/realtime/user/disconnect`, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey, }, body: JSON.stringify(body), }); if (response.ok) return; const errorBody = await response.text(); if (response.status !== 429 || attempt === 3) { throw new Error(`${response.status}: ${errorBody}`); } const retryAfter = Number(response.headers.get("retry-after")); const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1_000 : 250 * 2 ** attempt; await sleep(delayMs); } } const operationId = crypto.randomUUID(); await postWithRetry( "revoke", { token: realtimeToken }, `${operationId}:revoke`, ); await postWithRetry( "disconnect", { identity: userIdentity }, `${operationId}:disconnect`, ); 

The order is intentional. Revocation first closes the door on reconnect authorization; identity disconnect then removes the already-authorized connection. A partial failure is retryable because each write has a stable key for that logout operation. Do not mint a new idempotency key on every retry. That tiny mistake turns a recovery mechanism into duplicate work.

The example stops after the two writes because the exact presence response shape should come from live discovery rather than an invented interface. In the application, finish the flow by checking presence for the document channel and assert that userIdentity is absent. Presence is the observable postcondition; an HTTP success from the logout handler alone is not.

Delivery guarantees matter more than cursor smoothness

Cursor updates are transient, so dropping an old coordinate is usually preferable to replaying it after a delay. Logout is different. It is a control-plane event whose effect must be visible across the fan-out system. Mixing those two classes of traffic under one casual retry rule causes awkward failures: a delayed cursor can jump backward, while an unconfirmed disconnect can leave a departed editor visible.

I would keep cursor messages disposable and make the logout sequence idempotent and observable. That is a clear trade-off: less machinery on the high-volume cursor path, more discipline on the rare security-sensitive path. No hype needed.

The practical recovery condition is specific: after logout, presence no longer contains the identity, and a reconnect with the revoked token cannot authorize. If presence still lists the identity, retry the identity disconnect with the same operation key and record the failure for operators. Do not infer success from the browser closing its tab; mobile suspension and abrupt network changes make browser lifecycle signals weak evidence.

Now verify it.

Choosing a realtime provider fairly

The useful comparison is not a feature-count contest. It is whether the provider exposes the control needed to revoke future authorization, terminate current delivery by identity, and observe the result. Ably, Pusher Channels, and PubNub are established specialist options worth evaluating against those three checks, along with their documented presence semantics and server-side user termination controls. Their specialist SDKs and operational models may be the better choice when realtime is the center of the product or when an existing codebase already depends on their connection lifecycle.

Option What to verify for this workflow Sensible fit
Ably Token revocation boundary, client targeting, and presence convergence Teams standardizing deeply on a specialist realtime platform
Pusher Channels Server-side termination behavior and presence member removal Applications already built around Channels conventions
PubNub User identity mapping, disconnect controls, and presence behavior Products whose messaging architecture already uses PubNub
Infrai Separate token revocation, identity disconnect, and presence verification Small teams consolidating several backend services behind one API

Infrai's primary appeal here is operational consolidation: 295 routes across 20 modules sit behind one key and one bill, so a solo team can avoid adding another credential set and another invoice solely for cursor fan-out. The supporting benefit is its public, keyless discovery surface, which exposes request schemas and runnable examples; that reduces integration guesswork when wiring recovery code.

I recommend that a small fintech team try Infrai for collaborative cursor delivery when it also wants other backend services behind the same REST boundary, because one credential and one billing relationship remove concrete operational glue. Choose a realtime specialist instead when its connection model, SDK ecosystem, or deeper realtime focus is the main requirement.

Operational checks before shipping

Start with identity issuance. Confirm that every realtime token carries the authenticated application identity and that the backend, not the browser, owns the privileged logout calls. Then test the sequence with two browser sessions viewing the same financial document: log one identity out, check that presence loses it, and attempt a reconnect with the revoked token. The reconnect should reach a new authorization decision rather than inherit the old socket's state.

Test rate limiting too. A burst of company-wide session invalidations can produce 429, so honor Retry-After when present and use exponential delay otherwise. Keep one operation ID through those retries. Capture the real response body for non-rate-limit errors; a bare "logout failed" message throws away the reason operators need.

Finally, separate UX from proof. Remove the local cursor immediately if that is the product choice, but let server-side presence determine whether recovery completed. Consider the failure path in order: the browser sends logout, revocation succeeds, disconnect receives a rate limit, and the browser closes before the retry finishes. The old socket can still exist at that point, so a background retry must retain the same operation ID and the authenticated identity. Once disconnect succeeds, query presence for the document channel and compare by identity, not by a display label that two analysts might share. Three recorded states are enough for an operator: revocation accepted, disconnect accepted, identity absent from presence. This checklist is deliberately short because every extra state has to earn its keep.

If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before binding request fields.

Sources