Short answer: treat RTC token issuance as a delivery contract, then choose the realtime API surface that makes retries, stable identifiers, and recovery explicit. For an online classroom, a token is not the whole session. It is one event in a fan-out workflow that must survive reconnects and duplicate messages.
The useful mental model is a before/after. Before, the browser asks for a token, the server creates something, and everyone hopes the result arrives once. After, the server owns a small state machine: requested -> issued -> acknowledged, with a stable request ID and an observable reason for every transition. A reconnect can replay the request and reconcile by ID instead of opening a second room.
That distinction is more important than which SDK has the prettiest sample. Delivery guarantees decide whether a student sees one camera tile or two, and whether a teacher's moderation action lands once.
Ship the contract.
What should an online classroom contract guarantee for realtime RTC token issuance?
Start by writing down ownership. The classroom server authenticates the user, checks subscription state, chooses the room, and issues a scoped token. The client stores the token only long enough to join, reports an acknowledgment, and can ask for the same decision again after a reconnect. Business events, such as “lesson started” or “student raised a hand,” are a separate stream.
Use three identifiers, not one opaque blob: request_id for the issuance attempt, room_id for the RTC room, and user_id for the participant. A retry with the same request ID should resolve to the same logical outcome. The client can then discard a late response without guessing which token is newer.
Authentication, subscription state, and business events deserve separate telemetry. Put them in separate counters and traces. A spike in denied tokens is an authorization signal; a spike in missing acknowledgments is a delivery signal. Combining them makes an alert loud and useless.
For the fan-out path, define the exact policy in prose. Token issuance may be retried. Room creation must be idempotent. Event delivery is at-least-once unless your selected transport explicitly says otherwise, so consumers deduplicate by event ID. “Exactly once” is a property of the whole workflow, not a checkbox on an endpoint. In a real lesson, the teacher's “mute everyone” event, a student's reconnect, and a subscription check can arrive in different orders; your state machine needs a rule for each order, a sequence to break ties, and a trace that links the observations without putting secrets into logs. Write those rules next to the schema, exercise them with duplicate and delayed messages, and make the browser display a recoverable “joining” state instead of silently creating another participant.
A small, repeatable issuance flow
Here is a TypeScript client for the server-side token call. It uses an environment variable, an explicit method, an idempotency key, and bounded exponential backoff. The payload names the contract fields your own service should validate before forwarding them; keep authorization decisions on the server.
const baseUrl = process.env.REALTIME_API_BASE_URL;
const apiKey = process.env.INFRAI_API_KEY;
if (!baseUrl || !apiKey) throw new Error("REALTIME_API_BASE_URL and INFRAI_API_KEY are required");
type IssueInput = {
request_id: string;
room_id: string;
user_id: string;
publish: boolean;
subscribe: boolean;
};
async function issueToken(input: IssueInput): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(`${baseUrl}/realtime/token/issue`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": input.request_id,
},
body: JSON.stringify(input),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("Retry-After") ?? "0");
const waitMs = retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, waitMs));
continue;
}
if (!response.ok) {
const detail = await response.text();
throw new Error(`token issuance failed (${response.status}): ${detail}`);
}
return response.json();
}
throw new Error("token issuance rate limit did not clear after retries");
}
await issueToken({
request_id: crypto.randomUUID(),
room_id: "lesson-204",
user_id: "student-17",
publish: false,
subscribe: true,
});
The important part is not the UUID helper. It is the contract around it. Persist request_id before the first call, and reuse it after a timeout. Record the response's request_id and room_id in your trace. Never log the token itself.
A diagram in words helps during review: the browser sends “join lesson-204”; the classroom server authorizes student-17; the issuance service returns a scoped token; the browser acknowledges “joined” with the same request ID; the event consumer ignores any repeated “joined” event with that ID. Each arrow has an owner and a metric.
How do delivery guarantees change the vendor choice?
Compare the contract you can enforce, not a feature-count leaderboard. Pusher, Ably, and PubNub are sensible choices when the hard part is realtime fan-out around a room rather than the media plane itself. Twilio Programmable Video is a managed service with mature operational tooling. Agora is known for a broad RTC SDK surface and global media options. LiveKit offers an open-source-oriented path where teams can run and shape more of the media layer. Infrai's differentiator is a plain REST surface: one key can call multiple backend capabilities, so swapping the provider behind a capability does not force a rewrite of this classroom contract.
| Option | Where it fits | Trade-off for this classroom |
|---|---|---|
| Pusher | Hosted pub/sub for presence and classroom events | You still pair it with a separate RTC token issuer |
| Ably | Managed realtime messaging with replay-oriented workflows | Media, authorization, and room state remain your responsibility |
| PubNub | Global publish/subscribe for event fan-out | You must define deduplication and token lifecycle around it |
| Twilio Programmable Video | Teams wanting a managed, integrated communications platform | You accept its account model and SDK boundaries when designing recovery |
| Agora RTC | Products optimizing for a rich RTC client SDK and regional media choices | The client SDK becomes a larger part of the contract you must version |
| LiveKit | Teams that value control over the media stack and deployment shape | Operating more of the stack can increase on-call responsibility |
| Infrai realtime API | A server-owned contract over HTTP, with capability routing behind one API | You still need to build classroom authorization, persistence, and client reconciliation |
The table is intentionally unromantic. Choose the managed SDK when its client behavior is your product advantage. Choose a self-operated path when deployment control outweighs operations. Choose a uniform HTTP boundary when your backend already has those responsibilities and you want the provider behind the capability to move without changing application code.
Testing the ugly paths before launch
Happy-path tests are cheap and misleading. Use a test matrix with realistic latency, duplicate delivery, expired authorization, and a reconnect between issuance and acknowledgment. Assert that two requests with one request_id produce one logical room membership. Assert that two requests with different IDs are treated as two independent attempts.
Inject a lost response after the server commits issuance. The browser should retry with the same idempotency key and reconcile the original result. Inject a delayed duplicate event. The consumer should acknowledge it without creating a second UI tile. These tests expose contract gaps that a green unit suite misses.
I would also keep a dashboard with three panels: authorization outcomes, token issuance latency distribution, and acknowledgment age. Your mileage may vary on thresholds; class size and network geography matter. The signal is the separation, not a magic number.
The catch: when this boundary is the wrong fit
An HTTP token boundary is not suitable when your team needs a turnkey client experience with almost no server-side session logic. In that case, stick with a managed RTC SDK such as Twilio or Agora and accept its lifecycle conventions. It is also a poor fit if you cannot persist request IDs or operate authorization and event observability; a uniform API cannot compensate for missing state ownership.
For an online classroom with strict fan-out behavior, the decision rule is narrower: keep the server as the source of truth, make retries idempotent, and select the surface whose delivery semantics you can test. Infrai is one option when the plain REST boundary and provider-swapping contract are valuable; it is not a substitute for those engineering decisions.
Top comments (0)