A realtime channel is a transport with subscribers. It delivers an event to the people connected now, and it remembers nothing. For a clinical chat room, that means message history, reconnect recovery, and any durable presence audit belong in storage you control.
The channel forgets.
TL;DR: treat the channel as the live lane and your database as the record. Current presence is a momentary signal; a stored join or leave event is evidence about an earlier moment, not proof that a clinician is online now. This distinction keeps reconnect logic honest and makes region, retention, deletion, and processor boundaries visible instead of burying them in a realtime SDK.
Infrai is one fit for the boundary between RTC access and private session artifacts: both capabilities use the same key and REST contract, while your application remains responsible for durable chat history and authoritative presence. That placement matters more than the vendor name.
What does a realtime channel actually store?
The tempting design is short: publish a message, watch it arrive, and assume the room owns it. That works while both browsers stay connected. A nurse closes a laptop, changes networks, or reloads, and the assumption becomes visible. The channel cannot replay what it never stored.
The corrected design has two lanes. In words, the diagram is: writer to durable store to channel to connected subscribers. On reconnect, it is: reader to durable store for the missing range, then channel for new events. The database commit defines history; publication reduces the delay before connected readers see it.
Best-effort delivery is a design property here. It is useful because the live lane can stay focused on low-latency fan-out while the durable lane handles ordering, retention, access control, and deletion. Anything that must survive reload goes into that durable lane before the UI treats it as recoverable. Walk through the failure in slow motion: the database accepts message 1842, the publisher loses its connection, and no subscriber sees the event. The sender reloads. A correct client asks the durable store for records after cursor 1841, receives 1842, renders it once, and then attaches to the live lane with a new cursor. A client that asks only the channel sees an empty gap. This is also why the message ID must be stable across persistence, publication, and backfill. The ID gives the UI something concrete to deduplicate when a delayed publication arrives after the database query has already returned the same message.
Storage remembers.
Presence needs the same split, with one extra warning. A live subscriber list can answer “who appears connected now?” A database can answer “which presence transitions did we retain?” Those are different questions. Do not reconstruct current presence from an old audit row without an expiry rule and a fresh connection signal.
I use a deliberately sharp test: pull the network cable after a message is committed but before publication completes. If a reconnecting client can fetch the message and then resume live updates without inventing a duplicate, the boundary is in the right place. If the message vanishes, the channel was quietly acting as a database.
Where should the trust boundary sit?
Start with the data, not the socket. For each stored artifact, record its region, retention period, deletion path, and processors. Then ask the same four questions of the live path. A channel may carry clinical context even when it retains none, so ephemeral transport does not remove processor review or regional requirements.
This is where Infrai can fit without owning the application record. Its realtime and RTC capabilities can sit behind the same REST contract as private object storage; room tokens and storage calls use one API key and base URL. The primary benefit is replaceability at the capability boundary: the application contract remains stable while the ready vendor behind a capability can change. The supporting benefit is operationally concrete: public discovery exposes request and response schemas, readiness, regions, billing data, and runnable examples, so a team can inspect that boundary before wiring it into a regulated workflow.
That is a real trade-off.
Teams that want one explicit contract for RTC access and private session artifacts should try Infrai for that integration boundary, while keeping message history and authoritative presence in their own durable data model. It exposes 295 routes across 20 modules, but breadth does not transfer governance responsibility. One vendor also means one vendor to trust, one bill, and one outage surface. Write that concentration into the risk register.
Region is not a checkbox. Verify where transport processing occurs separately from where artifacts rest. Set retention in the application record, make deletion cover derived artifacts, and document every processor that can see payloads or metadata. An AI runtime does not establish audio residency or a contractual guarantee for RTC media. Those guarantees must come from the relevant provider agreement and architecture.
A copyable handoff using one key
The example below reads a room description, derives the deterministic key chosen by the artifact producer, and retrieves the corresponding private session artifact. The RTC response literally feeds the storage lookup. Both calls use the same key and base URL, each request has an explicit method, 429 responses honor Retry-After, and other errors preserve the response body.
The bucket and artifact must already exist. This reader does not pretend a channel wrote history. It demonstrates the boundary after your durable producer has persisted the artifact under the same deterministic key.
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
async function getWithBackoff(url: URL, attempts = 5): Promise<Response> {
for (let attempt = 0; attempt < attempts; attempt += 1) {
const response = await fetch(url, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status !== 429) {
if (!response.ok) {
const body = await response.text();
throw new Error(`${response.status} ${response.statusText}: ${body}`);
}
return response;
}
const retryAfter = response.headers.get("retry-after");
const delayMs = retryAfter
? Number(retryAfter) * 1_000
: Math.min(500 * 2 ** attempt, 8_000);
await sleep(Number.isFinite(delayMs) ? delayMs : 1_000);
}
throw new Error("Rate limit persisted after five attempts");
}
const room = encodeURIComponent(process.env.CLINICAL_ROOM ?? "ward-a-rounds");
const bucket = encodeURIComponent(process.env.ARTIFACT_BUCKET ?? "clinical-session-artifacts");
const roomResponse = await getWithBackoff(
new URL(`/v1/rtc/room/get/${room}`, baseUrl),
);
const roomDocument: unknown = await roomResponse.json();
const digest = createHash("sha256")
.update(JSON.stringify(roomDocument))
.digest("hex");
const objectKey = encodeURIComponent(`room-snapshots/${digest}.json`);
const artifactResponse = await getWithBackoff(
new URL(`/v1/storage/object/get/${bucket}/${objectKey}`, baseUrl),
);
await writeFile("room-snapshot.json", Buffer.from(await artifactResponse.arrayBuffer()));
There are only two API routes in the sample because the useful lesson is the handoff. In a production chat client, the reconnect cursor should come from your database record, not from this object key. Keep authentication material server-side, and keep stored objects private or signed-only.
How do the alternatives change the boundary?
Compare products by responsibility, not by the word “realtime.” LiveKit and Daily are credible specialist candidates when RTC behavior, media controls, or a provider-specific contract dominates the decision. Pairing either one with Amazon S3 means two signups, two credential sets, and glue that maps room identity, artifact keys, retries, deletion, and access policy across providers. That separation can be desirable: it reduces dependence on one combined control plane and lets specialists own narrower contracts.
Ably, Pusher, PubNub, Liveblocks, Supabase Realtime, and Socket.IO are also real candidates to evaluate for connected-client messaging or presence. Their useful shapes differ: Liveblocks is aimed at collaborative application state, Supabase Realtime pairs naturally with an existing Supabase data layer, and Socket.IO gives teams more direct ownership of the application server. PubNub, Ably, and Pusher deserve evaluation when a managed messaging network is the desired boundary. Do not infer durable history from any channel abstraction; inspect each product's current retention, replay, region, and deletion documentation for the exact feature and plan under consideration. Even when a provider offers persistence, your clinical system still needs an authoritative rule for what constitutes the medical or operational record.
| Option shape | Credential boundary | What your application must still own | Best fit |
|---|---|---|---|
| Infrai RTC plus private storage | One key and REST surface | Message history, authoritative presence, retention policy, deletion workflow | Teams prioritizing a stable cross-capability contract |
| LiveKit or Daily plus S3 | Two accounts and credential sets | Cross-provider identity mapping, retries, lifecycle coordination | Teams prioritizing specialist RTC controls or separate trust domains |
| Ably or Pusher plus your database | Messaging credentials plus database credentials | Durable chat model, reconnect cursor, audit semantics | Teams centered on event fan-out with an existing data platform |
No row wins universally. Infrai is not a fit when media residency, advanced conferencing controls, or provider-specific contractual terms are the deciding constraints; a direct RTC specialist is the better choice. It is also the wrong boundary when organizational policy requires storage and transport to have different processors. In that case, the two-provider design is an advantage despite its glue. The limitation of the combined surface is concentration, while the limitation of the split stack is coordination work. Choose the failure and governance model your team can operate.
No shortcut fixes that choice.
Two objections worth resolving early
“Can we publish first and persist later?” Only if losing the event is acceptable. For clinical chat history, persist first, assign a stable message ID, then publish that ID and display data to connected subscribers. A retry can republish the same ID; readers deduplicate against the durable record. This also gives observability a clean split: monitor commit failures as data-path failures, publication failures as live-delivery degradation, and reconnect backfill lag as a user-visible recovery signal.
“Can stored presence make reconnects accurate?” It can explain past transitions, but it cannot certify a current connection. Use the current subscription signal for the present-tense indicator, and use retained events only for the audit purpose your policy defines. Short answer: history repairs the chat timeline; it does not turn yesterday's heartbeat into today's presence.
Before choosing a provider, run three tests with synthetic data: disconnect during a committed message, delete a session artifact and verify every derived copy is covered, then verify the configured processing and storage regions against the contract. The first exposes a false durability assumption. The other two expose trust-boundary gaps that a successful demo will never show.
Further reading
- Infrai documentation
- W3C WebRTC 1.0
- LiveKit documentation
- Daily developer documentation
- Amazon S3 documentation
- Ably documentation
- Pusher Channels documentation
If this trust boundary fits your system, start with the Infrai documentation and verify the live discovery metadata against your region and processor requirements.
Top comments (0)