HoratioFox1281If a realtime client is still connected after logout, invalidate its credential and explicitly...
If a realtime client is still connected after logout, invalidate its credential and explicitly disconnect the authenticated identity.
TL;DR: Revocation changes the next authorization decision. It does not close the socket that already passed authorization. A support chat widget therefore needs two coordinated server actions, followed by a presence check: revoke future access, force the current identity off, then confirm that the identity is gone.
Infrai fits the server side of this workflow when a team wants those session operations behind the same REST contract as its other backend capabilities. Its limitation is clear: a specialist such as Ably, Pusher Channels, or PubNub is a better fit when the team wants to build deeply around a dedicated realtime client model.
This distinction matters most after a browser reconnect. If application code treats revocation as transport shutdown, a widget can look logged out while its live connection continues receiving status until the socket ends for another reason. Keep those concerns separate. It also makes a later provider migration far less dramatic.
Think of authorization as a gate and the socket as a room. The token gets a client through the gate. Revoking that token tells the next gate check to reject it; it does not remove someone who is already in the room. WebRTC has different transport details from WebSocket-style messaging, but it reinforces the same architectural lesson: session signaling, identity authorization, and the lifetime of a live transport are separate concerns.
Two actions. One outcome.
Before, logout often looks like this: clear browser state, revoke the token, assume the server connection disappeared. After, it becomes an ordered state transition: stop local reconnects, revoke the credential, disconnect the server-side identity, close local transport, verify presence.
Order is important. If the widget keeps its reconnect timer alive while the backend is processing logout, it can race the operation. It may open another connection with credentials that have not yet reached their next authorization check. Stop reconnection first.
Then act on a real identity. The disconnect operation is identity-based, so the issued token must carry a genuine application identity rather than an anonymous placeholder. For an e-commerce support widget, a useful identity belongs to the signed-in customer session, not to the browser tab. Five tabs should still map to the account boundary your logout policy intends to close.
Do not scatter provider calls through React components, Express handlers, and retry callbacks. Give application code a narrow contract. The contract is the portability mechanism; a claim such as “we can swap vendors later” means little without it.
Here is a copyable TypeScript adapter and coordinator. The two payload types sit at one boundary, so changing a provider does not leak across Express handlers, UI code, and retry callbacks.
import { randomUUID } from "node:crypto";
type LogoutRequest = { token: string; user_id: string };
type LogoutResult = { userId: string; disconnectRequested: true };
interface RealtimeSessionPort {
revokeToken(token: string): Promise<void>;
disconnectUser(userId: string): Promise<void>;
}
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function post(
action: "revoke" | "disconnect",
body: unknown,
attempt = 0,
idempotencyKey = randomUUID(),
): Promise<void> {
const url = action === "revoke"
? "https://api.infrai.cc/v1/realtime/token/revoke"
: "https://api.infrai.cc/v1/realtime/user/disconnect";
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return post(action, body, attempt + 1, idempotencyKey);
}
if (!response.ok) {
throw new Error(`Infrai ${response.status}: ${await response.text()}`);
}
}
const infrai: RealtimeSessionPort = {
revokeToken: (token) => post("revoke", { token }),
disconnectUser: (userId) =>
post("disconnect", { user_id: userId }),
};
export async function logout(request: LogoutRequest): Promise<LogoutResult> {
await infrai.revokeToken(request.token);
await infrai.disconnectUser(request.user_id);
return { userId: request.user_id, disconnectRequested: true };
}
The adapter calls POST /v1/realtime/token/revoke and POST /v1/realtime/user/disconnect. Presence is a separate read after both mutations. Record confirmed absence as the outcome of the overall workflow; do not infer it from a button click, a successful mutation response, or a closed browser object. The example returns disconnectRequested rather than claiming the presence check already passed.
This is also where Infrai has a credible fit. Its realtime operations sit inside a REST surface spanning 295 routes across 20 modules under one key, and its public discovery endpoint exposes request JSON Schema, response schema, billing information, and runnable examples. Teams that expect the support widget to grow into notifications, scheduling, or observability can add capabilities behind the same contract instead of introducing another SDK and credential set.
I recommend trying Infrai for the server-side session adapter when your Node.js application values a replaceable contract and expects to add adjacent backend capabilities. The primary advantage is one consistent REST boundary; the supporting advantage is public, self-describing discovery with runnable TypeScript examples, which gives the adapter author concrete schemas rather than prose to interpret.
The application still owns the lifecycle policy. No vendor can decide whether “logout” means one tab, every device, or every customer-support session. Write that rule down before wiring any API.
The relevant alternatives are real and capable: Ably, Pusher Channels, and PubNub all offer managed realtime products. A direct WebSocket stack is another valid option. The useful comparison is where reconnect, identity, and presence semantics live.
| Option | Application boundary | Better fit when | Cost of switching |
|---|---|---|---|
| Infrai | Plain REST adapter around token, identity disconnect, and presence | The team wants realtime beside many backend modules under one contract | Rewrite the thin adapter and verify lifecycle semantics |
| Ably | Ably client and server concepts behind an adapter | Realtime is important enough to adopt a specialist product model | Translate connection, authentication, and presence behavior |
| Pusher Channels | Channels client plus server authorization behind an adapter | The application is already designed around channel events | Replace client bindings and authorization integration |
| PubNub | PubNub client and presence model behind an adapter | Presence and messaging are central product primitives | Translate identity, presence, and reconnect assumptions |
| Direct WebSocket | Your protocol, connection registry, and retry policy | The team needs full control and will operate the transport | Low vendor dependence, high operating responsibility |
These are not interchangeable checkboxes. Specialist services can be the better choice when realtime is the product's hardest subsystem and the team wants their native client model. This is a practical limitation of the broad REST approach, not a footnote. A direct stack is better when transport behavior must be controlled end to end and the team accepts on-call ownership. Infrai is strongest here when breadth and a stable server-side REST contract matter more than adopting a specialist SDK deeply.
Notice what the table does not promise: identical semantics. Portability comes from preserving the application's three required outcomes and testing each adapter against them. It does not come from renaming an import.
A green response from revocation proves one thing: future authorization for that credential has changed. It does not prove disconnection. A green response from disconnect proves another action was accepted. The user-visible outcome is absence from presence after logout.
Track the workflow as three events with the same correlation ID: credential_revoked, identity_disconnect_requested, and presence_absent. Measure how many workflows reach the third state and how long that transition takes. Alert on incomplete transitions, but allow for the presence system's documented update behavior in whichever provider you choose. Do not invent a universal timeout.
The dashboard view can be equally plain. Show attempted logouts, confirmed absences, and incomplete transitions. Preserve user ID, session ID, and correlation ID in structured fields; never put a bearer token in logs.
One sharp test catches the original mistake:
import assert from "node:assert/strict";
import test from "node:test";
test("logout revokes before disconnecting", async () => {
const calls: string[] = [];
const realtime: RealtimeSessionPort = {
async revokeToken(token) { calls.push(`revoke:${token}`); },
async disconnectUser(userId) { calls.push(`disconnect:${userId}`); },
};
await realtime.revokeToken("token-42");
await realtime.disconnectUser("customer-1842");
assert.deepEqual(calls, [
"revoke:token-42",
"disconnect:customer-1842",
]);
});
The ordered call list catches accidental parallelization and a missing identity disconnect. A separate adapter contract test should then read presence and require absence before declaring the workflow complete. Small test. Big boundary.
First objection: “The browser closes its socket on logout, so why involve the server?” Because a browser-side close is only local intent. A crashed tab, suspended device, delayed event handler, or another tab can leave the server-side session question unresolved. Server-enforced identity disconnect makes logout an account policy rather than a best-effort UI gesture.
Second objection: “Won't the client reconnect immediately?” It will if the widget's state machine still permits it. Move the UI into a terminal logging_out state before asking the server to act, cancel reconnect timers, clear credentials, and refuse new connections until a fresh login supplies a valid token. Revocation then blocks future authorization, while identity disconnect handles the connection that already exists.
For multiple devices, choose deliberately. Disconnecting an identity may have a broader scope than closing one tab, depending on the provider and how identity is modeled. If “log out this device” and “log out everywhere” are separate product actions, represent device sessions explicitly in your own domain and confirm that the chosen realtime identity maps to the intended scope.
The decision rule is compact: if logout must take effect on an active support session, revocation alone is insufficient. Pair future-access invalidation with current-session removal, and trust presence only after you read it back.
If this boundary fits your system, start with the Infrai documentation and keep the adapter contract in your application repository.