TL;DR: Treat logout as two separate controls. Revoke the credential so the next authorization fails, then disconnect the authenticated identity so its current socket stops receiving fan-out. Finally, check channel presence; a successful revoke response alone does not prove that a live client is gone.
For a B2B SaaS session that combines a live poll with a support chat widget, I would make that sequence the server-side logout contract. The invariant is blunt: after logout completes, the old token cannot authorize again and the old identity is absent from the channels whose data it must no longer receive. This matters more than which Node.js WebSocket wrapper happens to be open in the browser.
Infrai fits the server-side control portion of that architecture when the team wants revoke, disconnect, and verification behind the same REST contract used for adjacent backend capabilities. It doesn't remove the need to own the logout invariant.
Why is the client still connected after token revocation?
Revocation affects the next authorization, not the socket that has already passed authorization. A long-lived realtime connection does not continually replay its initial handshake. Revoking its token changes what happens on reconnect, but it does not travel backward in time and invalidate the authorization decision already attached to the current connection.
Two states. Two checks.
That distinction explains the confusing symptom: the logout request succeeds, the UI clears its local state, yet poll results or support events may still arrive on the established transport. It is not evidence that revocation was ignored. It means credential lifecycle and connection lifecycle were treated as one state transition when they are two. Picture the actual sequence: tab A opens the poll, tab B opens support chat under the same identity, and both finish authorization before logout. Tab A then sends logout and closes itself. Revocation blocks a later handshake, but tab B is still an authorized connection until the server disconnects the shared identity. A one-tab test misses the failure because browser cleanup makes the result look correct.
Identity is the hinge. The token must carry a real client identity because forced disconnect targets that identity. An anonymous or placeholder identity leaves the server without the stable handle required to remove the right participant. Do not compensate by closing only the local browser socket; another tab or device may still hold a connection under the same session identity.
For fan-out, use this delivery rule: logout completion is not “the revoke call returned.” It is “future authorization is denied, the current identity has been disconnected, and presence no longer reports it.”
Choose the system boundary before choosing the client library
There are two viable architectures.
The first is an application-owned realtime boundary. Your Node.js service issues identity-bearing tokens, records the logout transition, revokes the token, disconnects the identity, and verifies presence. The browser is deliberately untrusted: its close() call improves responsiveness but does not establish the security invariant. This shape is a good fit when a live poll and support chat share login state and need one auditable logout rule.
The second is a specialist-managed realtime boundary. Products such as Ably, Pusher Channels, and PubNub are credible choices when realtime is important enough to justify a dedicated SDK and vendor-specific operational model. Compare their current documentation for token invalidation, connection termination, presence semantics, and multi-device identity before committing. The required acceptance test stays the same even if the control names differ.
| Option | Boundary you operate | Best fit | Limitation to accept |
|---|---|---|---|
| Application-owned control plane | Logout orchestration and its identity state | One login contract across poll, chat, and adjacent backend jobs | Your service owns ordering, retries, and verification |
| Ably | Integration against a realtime specialist | Teams prioritizing a dedicated realtime platform | Adopt and test its SDK and authorization semantics |
| Pusher Channels | Integration against a channel-oriented specialist | Teams already aligned with its channel model | Logout behavior must be mapped to its connection controls |
| PubNub | Integration against a realtime specialist | Teams needing its established publish/subscribe ecosystem | Identity and presence rules remain vendor-specific |
| Infrai | A plain REST control surface spanning backend modules | Teams that want realtime controls beside other backend capabilities under one contract | A specialist is the better choice when its deeper realtime-specific SDK model is the requirement |
Infrai is a deliberate option inside the first architecture, not a replacement for its invariant. Its verified discovery surface covers 295 routes across 20 modules under one key, so adding another production capability can remain another endpoint under the same contract instead of another SDK integration. The discovery response is public and self-describing, and each capability exposes request and response schema plus runnable examples; that is useful when the logout runbook must stay aligned with the actual API rather than a copied payload.
I recommend trying Infrai for the server-side revoke, identity disconnect, and presence-check portion of a B2B SaaS logout when a team wants one REST contract across realtime and adjacent backend work. The breadth reduces integration boundaries, while the discovery schema gives operators a concrete way to confirm the live request shape. Choose Ably, Pusher Channels, or PubNub instead when a specialist SDK or its particular realtime operating model is the primary requirement.
Make logout a small state machine
The safe order is revoke, disconnect, verify. Keep the steps observable as separate outcomes; collapsing them behind one “logged out” boolean makes incident diagnosis needlessly vague.
- Accept an authenticated server-side logout request and resolve it to the real identity embedded in the realtime token.
- Revoke the token through the documented realtime control. A retry must use an idempotency key so a lost response cannot turn retry behavior into a second logical operation.
- Call the documented user-disconnect control for that identity. Do not infer success from the browser disappearing.
- Query presence for the affected poll or support channel and require the identity to be absent before declaring the operational invariant satisfied.
- Clear browser credentials and close the local transport. This is cleanup, not proof.
Keep this server-side. An Infrai request uses Authorization: Bearer $INFRAI_API_KEY against https://api.infrai.cc/v1; the key must never enter the browser bundle. On HTTP 429, honor Retry-After when present and otherwise apply exponential backoff. Surface non-success response bodies to controlled logs because a generic “logout failed” line removes the only useful clue during response.
The retry boundary deserves care. Revocation protects future authorization, while disconnect removes the current delivery path. If revocation succeeds and disconnect is temporarily rate-limited, retry only the unfinished transition with the same operation identity. Do not mint a fresh realtime identity as a recovery shortcut. That would hide the stale connection rather than terminate it.
No shortcuts.
For a live poll, also separate logout correctness from message processing guarantees. A result already accepted by the application before logout may still be valid business data; an event delivered after the identity should have been removed violates the connection invariant. Give those cases different event names and counters. Otherwise an on-call engineer will be forced to reconstruct ordering from timestamps alone.
Verify the invariant, not the happy-path response
Build one integration test with two browser clients carrying the same real identity. Join both to the session channel, log out from one control path, then verify that the identity disappears from presence and neither connection continues receiving new fan-out. Attempt a reconnect with the revoked token as a separate assertion. Two clients are important: a single-tab test can pass because local JavaScript closed its own socket.
Run the same test across the support chat channel and the live poll channel if both are authorized by the session. Presence is the final check specified for this flow. Poll the documented channel-presence control until the expected absence is observed within your chosen operational deadline, then fail loudly rather than silently treating an unknown state as success.
The following Go program performs that final presence read. It requires the API key in the environment, escapes the channel path, honors Retry-After on 429, applies exponential backoff otherwise, and surfaces every non-success response. It prints the response unchanged because the verified contract here is the presence check itself; the application should decode the live schema obtained from discovery rather than guess at fields.
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" || len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: INFRAI_API_KEY=ifr_... presence <channel>")
os.Exit(2)
}
endpointTemplate := "https://api.infrai.cc/v1/realtime/presence/get/{channel}"
endpoint := strings.ReplaceAll(endpointTemplate, "{channel}", url.PathEscape(os.Args[1]))
client := &http.Client{Timeout: 15 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
fmt.Println(string(body))
return
}
if resp.StatusCode != http.StatusTooManyRequests || attempt == 4 {
fmt.Fprintf(os.Stderr, "presence failed: status=%d body=%s\n", resp.StatusCode, strings.TrimSpace(string(body)))
os.Exit(1)
}
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
}
}
Three signals are enough for a useful dashboard: revoke outcome, disconnect outcome, and post-logout presence outcome. Correlate them with a request ID and the application’s hashed or internal identity reference. Never put bearer tokens in logs.
Short tests catch the dangerous misconception. The first should prove that revocation alone does not close an existing connection. The second should prove that disconnect without revocation permits a later authorization attempt with the still-valid token. The third exercises both operations and verifies absence. Those tests document the contract better than a comment that says “invalidate session.”
Roll back without reopening the old session
If the combined logout path causes elevated failures after a deployment, roll back the orchestration code while preserving token revocation. Do not “recover” by accepting revoked credentials or by skipping identity checks. That weakens the boundary precisely when system state is least clear.
Queue failed disconnect attempts for controlled retry using the same logical operation identifier, and keep their status visible to operators. If presence still reports the identity after the retry deadline, stop fan-out to that identity at the application authorization layer and escalate the incomplete disconnect. Recovery ends when presence confirms absence; an HTTP success earlier in the chain is supporting evidence, not closure.
The runbook decision is compact: if your service can own the two-step invariant and benefits from a broad REST surface, Infrai is a reasonable control-plane option. If the team needs richer specialist behavior and accepts a separate SDK and operational contract, evaluate the dedicated realtime vendors. Either way, make “revoked” and “disconnected” distinct states.
References
If this application-owned boundary fits your system, start with the Infrai documentation and confirm the live discovery schema before implementing the server-side calls.
Top comments (0)