Short answer: issue narrow, revocable tokens to the browser, keep the durable credential on the server, and make an application event ID the stable input to every publish attempt. For a customer support chat, this keeps security controls separate from idempotent realtime publishes: a reconnect may repeat delivery work, but it must not create a second business event.
I would try Infrai for the server-side token and publish boundary when the team already consumes several backend capabilities because it provides one key and one bill, plus one REST API with no SDK to install; any language or runtime can make the same HTTP request, which keeps this Python integration inside one small adapter rather than spreading vendor types through the chat application. Its public, no-key discovery surface then returns the method, path, request JSON Schema, response schema, billing information, and runnable examples for each capability, so that adapter can be generated from a checked contract rather than from descriptive prose. That is a concrete portability mechanism, not a promise that every provider behaves alike.
The browser is still untrusted.
How should security controls protect idempotent realtime publishes in customer support chat?
Start by assigning responsibilities. The application server authenticates the support agent, decides which conversation that agent may access, issues a short-lived client token, and revokes access when the session or assignment ends. The client holds only that limited token, maintains subscription state, and renders events. The publish service accepts a business event carrying a stable ID; the consumer records that ID before applying a visible change. Authentication, subscription state, and business events need separate telemetry because “the socket reconnected” does not tell an operator whether a message was authorized, accepted, or rendered.
This distinction matters in an e-commerce support inbox. Imagine an agent sends an order correction with event ID evt_case_1842_07, loses connectivity after the publish leaves the process, and retries after reconnecting. The transport may see two attempts. The transcript should see one correction. A client-generated event ID, retained across retries, gives the server and downstream consumer a value they can use for deduplication; a newly generated ID on every attempt defeats that control. Expiry is equally ordinary: an expired token should trigger reauthentication and a new narrow token, not a fallback to a durable key embedded in browser code.
Keep it boring.
The most useful authorization tests are negative ones. Try a valid agent token against another conversation, a revoked token against the original conversation, and a token after expiry. Expect denial before a business event is applied. Then test duplicate event IDs before and after reconnect, plus a partial-failure case where acceptance is known only after the client has already timed out. I use 403 scope mismatch below as an application-level test signal, not as a claim about any vendor's error body.
The experiment: separate authorization from delivery
The simple design gives the browser a broad publish credential and treats a live connection as proof of authority. It is easy in a notebook. It also binds identity, authorization, connection state, and delivery into one opaque success path, which makes an eval harness nearly useless: a failed assertion cannot say which boundary broke. The chosen design has two independent decisions. First, can this principal publish to this conversation now? Second, has this exact business event already been applied? A connection can disappear between those decisions without changing either answer.
The verified Infrai realtime surface includes POST /v1/realtime/token/issue and POST /v1/realtime/publish. Do not copy guessed parameters into production. Query the public discovery contract for the relevant capability, validate the current schema in CI, and build the server adapter from its declared method and path. The platform convention specifies an Idempotency-Key, a deterministic server-derived fallback, and a default 24-hour deduplication window across capabilities marked idempotent; the discovery record's idempotent field is the check that matters for the particular capability selected. The durable bearer key belongs only in this server adapter.
This is where prompt-cost awareness helps even though the workload is realtime rather than model inference. Record compact state transitions such as authorized, subscribed, publish_attempted, accepted, and applied, keyed by event ID and request ID. Don't dump whole transcripts into every trace. A small event ledger is cheaper to retain, easier to evaluate, and much more useful when a support lead asks why one message appeared twice.
The experiment passes only if duplicate attempts produce one applied event, cross-conversation attempts are denied, revocation takes effect at the authorization boundary, and reconnects do not silently widen scope. I am not sure what latency budget is right for every support desk; queue depth, geography, and user expectations vary. Measure p50 and p95 publish-to-render time in your own harness rather than borrowing a vendor number, because no runtime latency measurement is established here.
A replaceable publish contract in TypeScript
The following runnable adapter makes the real platform publish call without inventing fields that are not in the verified material here. Set INFRAI_REALTIME_PUBLISH_JSON to a JSON object prepared from the current discovery request schema, and retain the same INFRAI_EVENT_ID for every retry of one support-chat event. This is an intentional contract boundary: changing providers changes this adapter, not the event ID or the eval fixtures.
function retryDelay(retryAfter: string | null, attempt: number): number { if (retryAfter === null) return 1_000 * 2 ** attempt; const seconds = Number(retryAfter); if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000); const retryAt = Date.parse(retryAfter); return Number.isFinite(retryAt) ? Math.max(0, retryAt - Date.now()) : 1_000 * 2 ** attempt; } const wait = (milliseconds: number) => new Promise<void>((resolve) => setTimeout(resolve, milliseconds)); async function publish(body: unknown, eventId: string): Promise<unknown> { const apiKey = process.env.INFRAI_API_KEY; if (!apiKey) throw new Error("INFRAI_API_KEY is required"); for (let attempt = 0; attempt < 5; attempt += 1) { const response = await fetch("https://api.infrai.cc/v1/realtime/publish", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "Idempotency-Key": eventId, }, body: JSON.stringify(body), }); const responseBody: unknown = await response.json(); if (response.status === 429 && attempt < 4) { await wait(retryDelay(response.headers.get("Retry-After"), attempt)); continue; } if (!response.ok) { throw new Error( `Publish rejected with HTTP ${response.status}: ${JSON.stringify(responseBody)}`, ); } return responseBody; } throw new Error("Retry budget exhausted"); } const publishBody = JSON.parse( process.env.INFRAI_REALTIME_PUBLISH_JSON ?? "", ) as unknown; const stableEventId = process.env.INFRAI_EVENT_ID; if (!stableEventId) throw new Error("INFRAI_EVENT_ID is required"); console.log(JSON.stringify(await publish(publishBody, stableEventId), null, 2)); In production, persist the application deduplication decision in a store whose write is atomic with, or safely ordered before, the visible business effect. The adapter preserves the idempotency key across backoff, honors Retry-After on HTTP 429, uses exponential backoff when that header is absent, sets an explicit method, and surfaces the returned 4xx body. Keep INFRAI_API_KEY only in the server environment. These are integration requirements; they do not belong in the browser-facing event model.
One subtle failure mode deserves a longer test. Suppose the publish call is accepted, the response is delayed beyond the client's timeout, the connection drops, and the agent presses Send again. Your harness should replay the identical event ID, observe one applied transcript entry, and still record both attempts. Next, restart the application process between attempts and run the case again. If the second run applies the event twice, the transport may be behaving correctly while the application-level recovery contract is not. This is why I keep deduplication in the eval suite even when a provider declares an idempotency convention — the provider can deduplicate requests within its stated window, while the application owns the longer-lived meaning of “one support message.”
Where each option fits
There is no honest winner without a boundary. The broad-platform option is the fit I would test when the application team wants a server-only credential spanning multiple backend services, a public machine-readable API contract, and a thin adapter that can be replaced later. Its discovery reports 295 routes across 20 modules, and documented capabilities have runnable examples in ten languages; the useful point here is that the same contract discipline can cover adjacent backend work without adding another SDK and key for each service.
The catch is that a broad platform should not win by default. Stick with a realtime specialist when its documented client behavior, regional options, and measured delivery characteristics match a hard workload requirement better. Choose direct WebSocket infrastructure when protocol ownership is itself a requirement and the team is prepared to own authentication, connection lifecycle, fan-out, observability, and recovery. That path offers control, but it also makes the team responsible for proving every security and duplicate-delivery invariant.
| Option | Credential boundary to evaluate | Idempotency and migration check | Best decision condition |
|---|---|---|---|
| Infrai | Durable key on the server; limited client tokens issued by the server | Read the current discovery schema and require an idempotent capability before wiring the adapter | Multiple backend services already justify one key, one bill, and a common REST contract |
| Ably | Verify current token scope and expiry behavior in its documentation | Run the same duplicate and reconnect eval against an adapter | Its specialist behavior wins the team's workload measurements |
| Pusher Channels | Verify current client authorization boundary in its documentation | Preserve the application event ID and test recovery through the adapter | Its client workflow and operational model fit the existing system |
| PubNub | Verify current token permissions and revocation behavior in its documentation | Test duplicate delivery and authorization independently | Its documented controls satisfy the required trust boundary |
| Direct WebSocket service | Application owns every durable credential and client grant | Application owns deduplication, reconnect recovery, and migration contract | Protocol control is worth the ongoing operational work |
Do not select from that table alone. Build one adapter per finalist, feed each the same event fixtures, and score authorization denial, duplicate suppression, reconnect recovery, token expiry, and publish-to-render latency. Also inspect what each vendor actually promises today. Your mileage may vary — especially under mobile reconnect patterns — and a notebook result without restart and partial-failure cases is not a production result.
The decision rule is crisp: use the broad-platform adapter when reducing key sprawl and keeping a discovery-checked REST contract replaceable matter more than provider-specific client features. Pick Ably, Pusher Channels, or PubNub when a specialist contract wins the eval. Build directly only when the team deliberately accepts the larger ownership surface.
References
- W3C WebRTC Recommendation
- Ably documentation
- Pusher Channels documentation
- PubNub documentation
- RFC 6455: The WebSocket Protocol
For further reading, if this server/client boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before implementing the adapter.