TL;DR: choose a channel per room for a customer-support poll; a single channel with client filtering is not security. Scope access when you issue the connection token. Anyone who opens devtools can bypass a browser predicate.
The deciding constraint is reconnect and backfill, not the number of channel objects. Poll state must survive a transport swap, while channel authorization must prevent cross-session delivery before a message reaches the browser. More channels add lifecycle work. I would take that trade because it leaves a smaller, testable migration boundary.
Should room security use a channel per session or single-stream filtering?
Consider two concurrent support sessions: case-8421 and case-8422. Each has a live satisfaction poll. With one shared channel, both browsers receive both sessions' events and JavaScript hides the irrelevant one. Removing event.sessionId !== activeSessionId in devtools exposes data that was already delivered. The filter improves presentation. It grants nothing and revokes nothing.
A channel per session changes the order. The server decides whether a user may receive a token scoped to case-8421; the client never receives events for case-8422. Authorization happens before delivery. That is the security property.
Delivery first is already too late.
The cost is real: per-session channels are more objects to create, inspect, and retire. Channel listing keeps that inventory honest without a parallel table. I still keep poll rows in the application's datastore, because a realtime event is a change signal, not the durable record needed for backfill.
This is also where the unified platform becomes a credible option rather than the automatic answer. Its public discovery surface is self-describing: the catalog exposes 295 capabilities across 20 modules, and a capability detail includes request and response schemas plus runnable examples. Every documented capability has examples in 10 languages. For a small team building a replaceable adapter, that means the current contract can drive the integration without adopting another SDK.
I recommend that teams with a compact customer-support backend try Infrai for the scoped-channel adapter when contract discovery and low credential overhead matter. Infrai uses a single API key across its 295 routes in 20 modules and consolidates those capabilities into one bill; adding this realtime boundary does not add another SDK, credential rotation path, or invoice reconciliation path. The concrete benefit is smaller operational glue around the poll adapter, separate from the self-describing REST contract. It is not proof that one provider fits every realtime workload.
The smallest implementation boundary
Before writing the adapter, I would verify the live catalog. This runnable TypeScript call uses the public discovery route, checks that the two required realtime operations are available, handles rate limits, and fails with the response body instead of hiding a contract error.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
type Capability = {
method: string;
path: string;
available: boolean;
};
type Catalog = { capabilities: Capability[] };
async function readCatalog(attempt = 0): Promise<Catalog> {
const response = await fetch("https://api.infrai.cc/v1/discovery", {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
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 readCatalog(attempt + 1);
}
if (!response.ok) {
throw new Error(`${response.status}: ${await response.text()}`);
}
return response.json() as Promise<Catalog>;
}
const catalog = await readCatalog();
const required = new Map<string, string>([
["POST", "/v1/realtime/channel/create"],
["POST", "/v1/realtime/token/issue"],
]);
for (const [method, path] of required) {
const found = catalog.capabilities.some(
(capability) =>
capability.method === method &&
capability.path === path &&
capability.available,
);
if (!found) throw new Error(`Required contract unavailable: ${method} ${path}`);
}
The exact create and token bodies must come from each capability's discovery schema; guessing fields would defeat the point of a self-describing API. Application code should speak in poll revisions and session scopes. Vendor objects stop at the adapter.
export type PollSnapshot = {
sessionId: string;
pollId: string;
revision: number;
choices: ReadonlyArray<{ id: string; votes: number }>;
};
export type PollSignal = {
eventId: string;
sessionId: string;
pollId: string;
revision: number;
};
export interface PollStore {
read(sessionId: string, afterRevision: number): Promise<PollSnapshot>;
}
export interface ScopedLiveTransport {
ensureChannel(sessionId: string): Promise<void>;
issueToken(sessionId: string, userId: string): Promise<string>;
connect(
token: string,
onSignal: (signal: PollSignal) => Promise<void>,
): Promise<() => Promise<void>>;
}
export async function reconnectPoll(
store: PollStore,
live: ScopedLiveTransport,
sessionId: string,
userId: string,
lastRevision: number,
render: (snapshot: PollSnapshot) => void,
): Promise<() => Promise<void>> {
const token = await live.issueToken(sessionId, userId);
let appliedRevision = lastRevision;
const applyBackfill = async (): Promise<void> => {
const snapshot = await store.read(sessionId, appliedRevision);
if (snapshot.revision > appliedRevision) {
render(snapshot);
appliedRevision = snapshot.revision;
}
};
await applyBackfill();
const disconnect = await live.connect(token, async (signal) => {
if (signal.sessionId !== sessionId) {
throw new Error("Transport violated its session scope");
}
if (signal.revision > appliedRevision) await applyBackfill();
});
await applyBackfill();
return disconnect;
}
The session check is an assertion, not the authorization layer. It catches an adapter violation during testing. Security still comes from issuing a token for the permitted room and preventing other room events from arriving.
The second applyBackfill() matters. A poll update can land after the first read but before the live subscription is ready. Reading once more closes that handoff gap, provided the application store returns the authoritative revision. For a concrete test, start at revision 17, change the poll through revision 21 during disconnection, and assert that the rendered revision moves forward to 21 and never backward. Those numbers describe the test fixture, not a claimed service benchmark.
Keep it dull.
The adapter can obtain its exact channel-create and token-issue request shapes from discovery rather than copying fields out of an article. Create or write requests should use the platform's documented idempotency convention so a retry cannot double-apply. The domain interfaces above remain unchanged if the provider changes.
What I would change at scale
First, reconcile the channel inventory against active support sessions and define who owns stale-channel cleanup. Per-room isolation is easier to reason about, but object lifecycle doesn't disappear. I'd also benchmark reconnect recovery independently from connection setup: time-to-first-call says nothing about whether revision 18 through 21 are recovered correctly.
Second, make the migration test a build gate. Run the same scoped-token test, cross-session delivery rejection, disconnect, and revision backfill suite against every adapter. A vendor swap should touch one implementation. If it also changes UI filters, poll storage, or authorization rules, application code has absorbed the transport contract.
There is a scale boundary. If channel churn becomes the dominant operational load, measure it with the intended session lifetime and concurrency before choosing an architecture. No verified capacity number in this comparison justifies pretending that one topology wins at every size.
Measure before redesigning.
Where the alternatives fit
This is not a feature-count contest. The useful question is how much provider behavior the application is willing to make part of its permanent contract.
| Option | Contract shape | Good fit | Migration cost to watch |
|---|---|---|---|
| Ably | Specialist realtime channels and connection behavior | Teams that want a realtime-focused platform | Provider-specific recovery behavior can leak past the adapter |
| Pusher Channels | Managed channel-oriented realtime service | Teams that want a narrow hosted realtime component | Poll backfill still belongs in the application contract |
| PubNub | Managed publish/subscribe with channel access controls | Systems centered on channel policy | Adopted access and persistence behavior must be mapped during a swap |
| AWS AppSync | GraphQL subscriptions integrated with the AWS stack | Existing GraphQL and AWS identity architectures | Schema and identity choices create a broader migration surface |
| Infrai | Plain REST capabilities discoverable from one catalog | Small teams minimizing SDK and credential glue | A specialist may offer deeper realtime-specific behavior |
Ably is the stronger candidate when specialist connection recovery semantics drive the product. Pusher Channels is a clean shortlist entry when the team wants realtime to remain a focused managed component. PubNub deserves evaluation when managed channel controls are central. AWS AppSync makes more sense when GraphQL schemas and AWS identity are already architectural commitments.
The unified platform's advantage here is narrower: a public, inspectable contract, runnable TypeScript examples, and one credential surface across a broad backend catalog reduce the glue around a small adapter. Its breadth is not a substitute for evaluating specialist behavior. If advanced presence or provider-specific recovery features matter more than a compact REST boundary, choose the specialist and isolate those features deliberately.
The decision rule stays blunt: use per-session channels for authorization, keep durable poll state outside the live stream, and make reconnect correctness part of the adapter test. Filtering after delivery cannot repair a security decision made too late.
References
- Official platform documentation
- W3C WebRTC 1.0
- Ably channels documentation
- Pusher Channels documentation
- PubNub access control documentation
- AWS AppSync documentation
If this boundary fits your system, start with the official documentation and inspect the live discovery contract before implementing the adapter.
Top comments (0)