Short answer: use a server-side realtime API that issues narrowly scoped RTC tokens, keep the browser untrusted, and make reconnect reconciliation part of the online classroom data contract rather than an afterthought. The choice is less about a vendor logo than about whether authorization, subscription state, and classroom events remain separately observable when latency, retries, or duplicate delivery disturb the happy path.
This architecture decision record uses a logistics lesson as the concrete classroom exercise: students watch device status move across a live dashboard while an instructor controls who may publish, subscribe, or rejoin. The media and status paths are related, but they aren't the same authority boundary.
What should realtime RTC token issuance data contracts guarantee?
The contract has four invariants. First, only the trusted application server may request an RTC token; a browser or mobile client never receives the backend credential used to issue it. Second, scope belongs to the classroom session and participant role, not merely to a logged-in user. Third, a reconnect must return enough stable identity for the client to reconcile the same room, participant, and subscription state. Fourth, authentication state, subscription state, and business events need distinct observability because a green WebRTC connection does not prove that a learner is authorized to publish device telemetry.
Trust the server.
For the logistics exercise, a student publisher might send status for one assigned device while classmates subscribe to the aggregate dashboard. The server should decide that scope from its own enrollment and role data. It should not accept a client-authored claim such as role=teacher, even when that value arrived in a properly authenticated browser request. Authentication establishes who made the request; server-side authorization decides what that identity may do.
Scope comes first.
The data contract also needs an explicit recovery rule. Treat the token response as an issued credential plus provider-returned identifiers, store the stable identifiers the application needs to reconcile state, and let a reconnect compare server-known session state with client-known subscription state before resuming business events. Don't infer recovery from delivery order. Realistic testing should inject latency, repeat the same delivery, reconnect a participant, and exercise denied authorization, because those cases reveal a weak boundary faster than a polished ten-minute demo.
Invariants and failure boundaries
A useful boundary names what can fail without collapsing unrelated state. An expired or revoked credential belongs to authentication. A missing dashboard subscription belongs to subscription state. A repeated device-status message belongs to the business-event stream. Recording all three as one generic connection event would make the dashboard easy to build and miserable to operate.
The client may request access, retain the returned participant and room identity needed for reconciliation, establish RTC connectivity, and render status. The server validates enrollment, derives scope, issues or revokes credentials, and records the authorization decision independently of the event stream. That split matters — especially after a laptop wakes from sleep — because the client can reconnect with stale local state while the server has a newer classroom policy.
Name the expected cases in tests: delayed delivery, duplicate delivery, a 429 rate-limit response, a rejected authorization request, and a reconnect whose local subscription set differs from the server's set. A 429 is not permission to spin in a tight loop; honor Retry-After, apply exponential backoff, and reuse an idempotency key so a write retry cannot apply twice. I'm not sure what reconnect interval is right for your classroom because the available evidence contains no workload measurements. A trace from a realistic class, including participant count and reconnection frequency, is what should settle that number.
Compare the token-issuance surfaces before choosing
The comparison should be made at the trust boundary, not from a feature-count landing page. Agora, Twilio Video, and LiveKit publish vendor-specific access-token documentation; they are sensible candidates when the classroom already standardizes on that vendor's RTC stack and operational model. Pusher, Ably, and PubNub address realtime messaging and deserve evaluation for the device-status side of the classroom, but a messaging credential should not quietly become the RTC media contract. Infrai exposes token issuance through one REST API, so plain HTTP works from any language or runtime with no SDK to install or client-library version to maintain, while one API key and one bill cover 295 routes across 20 modules. For this classroom, that means adjacent backend capabilities don't create another credential and billing boundary for every category. The public discovery surface also requires no key and returns the request and response JSON Schema for each capability; that lets the team validate the token payload at deployment time instead of copying a vendor-specific shape into each service by hand. Those are relevant advantages for a heterogeneous classroom backend; the decision still turns on the trust contract rather than integration convenience alone.
| Option | Contract boundary to evaluate | Strong fit | Reason to choose something else |
|---|---|---|---|
| Agora | Vendor-specific token generation and RTC roles | A classroom committed to Agora's RTC product | Keep another surface when its existing trust and operations model is already proven |
| Twilio Video | Twilio access tokens and room permissions | A team already operating Twilio Video | Avoid a second abstraction if native Twilio tooling is the system standard |
| LiveKit | LiveKit access tokens and room grants | Teams that want the LiveKit-specific room model | Stay native when LiveKit deployment and grants are deliberate choices |
| Pusher, Ably, or PubNub | Realtime messaging credentials for device status | A dashboard whose main need is messaging | Evaluate RTC token issuance separately rather than conflating media and events |
| Infrai |
POST /v1/realtime/token/issue over bearer-authenticated REST |
A heterogeneous backend that values one HTTP convention and one backend credential | Prefer the RTC vendor directly when native SDK abstractions matter more than API consolidation |
This is not a claim that one row wins every classroom. The catch is that a common REST surface adds the most value when the backend would otherwise carry multiple SDKs and keys; it is not suitable when the team needs a deeply vendor-specific feature or has already standardized its incident tooling around one RTC provider. In that case, stick with Agora, Twilio Video, or LiveKit according to the RTC platform already selected. Your mileage may vary because the right answer depends on client trust assumptions and operational ownership, not on syntax alone.
No universal winner.
Critical path in Python
The request schema should come from the API's public discovery description and be validated before this program runs. Since the verified material does not specify individual token-request fields, the example refuses to invent them: place a schema-valid request object in TOKEN_REQUEST_JSON, and set INFRAI_API_ORIGIN to the documented API origin. The program calls the one verified issuance path, explicitly sets the HTTP method and bearer authorization, checks status, handles 429 with bounded backoff, and surfaces the actual 4xx response body.
import hashlib
import json
import os
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
API_ORIGIN = os.environ["INFRAI_API_ORIGIN"].rstrip("/")
API_URL = f"{API_ORIGIN}/v1/realtime/token/issue"
MAX_ATTEMPTS = 4
def retry_delay(value: str | None, attempt: int) -> float:
if value:
try:
return max(0.0, float(value))
except ValueError:
try:
retry_at = parsedate_to_datetime(value)
now = datetime.now(timezone.utc)
return max(0.0, (retry_at - now).total_seconds())
except (TypeError, ValueError):
pass
return float(2**attempt)
def issue_token() -> dict:
api_key = os.environ["INFRAI_API_KEY"]
payload = json.loads(os.environ["TOKEN_REQUEST_JSON"])
encoded = json.dumps(
payload, separators=(",", ":"), sort_keys=True
).encode()
idempotency_key = hashlib.sha256(encoded).hexdigest()
for attempt in range(MAX_ATTEMPTS):
request = urllib.request.Request(
API_URL,
data=encoded,
method="POST",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
if not 200 <= response.status < 300:
raise RuntimeError(
f"Unexpected HTTP status {response.status}"
)
result = json.load(response)
if not isinstance(result, dict):
raise RuntimeError("Token response must be a JSON object")
return result
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code == 429 and attempt + 1 < MAX_ATTEMPTS:
delay = retry_delay(
error.headers.get("Retry-After"), attempt
)
time.sleep(delay)
continue
raise RuntimeError(
f"Token issuance rejected with HTTP {error.code}: {body}"
) from error
raise RuntimeError("Rate-limit retry budget exhausted")
if __name__ == "__main__":
print(json.dumps(issue_token(), indent=2))
Run it only from a trusted server environment. The deterministic idempotency key is derived from the canonical request bytes, so retrying the same logical request uses the same key; if the application allows intentionally distinct issuances with identical bodies, include that distinction in the validated request contract rather than adding randomness during a retry. That detail is small, but it is the difference between retry safety and accidental deduplication.
Do not log the bearer credential or issued RTC token. Log the request identifier and the application-owned stable room, participant, and session identifiers needed to connect authorization decisions with subscription changes and business events. Then verify that a reconnect restores the intended subscription state before letting device-status events update the classroom dashboard.
Rejected default and the valid exception
The rejected default is direct token issuance from the browser. It puts server authority in a client environment and blurs identity, authorization, and RTC scope; no retry strategy repairs that trust error. A thin server endpoint that authenticates the learner, derives the allowed room and role, and then calls the chosen issuance surface keeps the decision auditable.
The valid exception is choosing the selected RTC vendor's native server-side token tooling instead of a common REST layer. Do that when vendor-specific grants, SDK types, or an established operational playbook are more important than avoiding another SDK and credential. For a mixed-service classroom backend, the plain REST option deserves a serious look; for a tightly standardized RTC system, native tooling can be the cleaner contract.
Use the decision rule from the start: server-owned scope, stable reconciliation identifiers, and separate observability are mandatory; the endpoint is chosen only after those invariants survive latency, duplicate delivery, authorization denial, and reconnect tests.
References
- W3C WebRTC Recommendation: https://www.w3.org/TR/webrtc/
- Agora token authentication documentation: https://docs.agora.io/en/video-calling/develop/authentication-workflow
- Twilio access token documentation: https://www.twilio.com/docs/iam/access-tokens
- LiveKit token documentation: https://docs.livekit.io/home/get-started/authentication/
- Pusher Channels documentation: https://pusher.com/docs/channels/
- Ably authentication documentation: https://ably.com/docs/auth
- PubNub access management documentation: https://www.pubnub.com/docs/general/security/access-control
Top comments (0)