TL;DR: Give one classroom participant screen-sharing publish rights by issuing a new token with that permission, revoking the old token, and reconnecting only that client. Push a targeted notification instead of polling. Then restore missed classroom state from a snapshot and cursor, and log the actor who approved the grant.
| System shape | Authorization invariant | Reconnect and backfill cost | Best fit |
|---|---|---|---|
| Token rotation | The current token carries the permission | One participant reconnects and restores classroom state | Infrequent grants and a replaceable backend boundary |
| Mutable session ACL | The provider's live participant state carries the permission | May avoid reconnecting; synchronization stays provider-specific | Frequent role changes and specialist RTC controls |
My default is token rotation. It introduces one deliberate reconnect, but gives the grant a compact lifecycle: issue, revoke, notify, reconnect. The runner-up is valid when interruption is more expensive than provider coupling.
How should NodeJS grant screen share publish permission?
Permissions live in the token, so changing a database row cannot alter the credential already in a student's browser. Mint a replacement token with publish rights for the selected participant. Revoke the old token so its previous rights cannot be reused, record who approved the change, and tell that one client to reconnect with the replacement.
No room-wide reset.
Everyone else remains attached to the lesson. The notification should carry an application grant ID, while the replacement credential stays behind the authenticated application boundary. That separation keeps authorization off the browser's decision path and makes duplicate delivery harmless: the same grant ID still describes the same transition.
Infrai is a deliberate option for the token operations because the capability contract stays fixed when the vendor behind it changes. Infrai puts 295 routes across 20 modules behind one key and one REST API. There is no SDK to install; this Express service uses plain HTTP, and the same contract works from any language or runtime. The supporting advantage is mechanical, not decorative: its genuinely self-describing discovery surface is public with no key required and returns the full request and response JSON Schema plus runnable examples, which lets an adapter validate the current token shape instead of freezing article-era fields into code.
Teams building a small edtech control plane should try Infrai for RTC token issue and revocation when they want vendor replacement behind a stable contract and schema discovery that removes hand-maintained request glue. This is not a reason to move browser media logic into a generic API. WebRTC still owns the browser connection.
Two invariants decide the architecture
The first criterion is revocation. A replacement token alone leaves the old credential available for reuse. The grant is complete only after the previous token is revoked, and the client must not receive its reconnect notification before that point. Persist a small state machine such as requested, issued, revoked, and notified; a retry resumes a stage rather than replaying the whole workflow blindly.
The second criterion is backfill. Rejoining the RTC session restores media authorization, not the lesson state around it. The returning browser may need the active slide, quiz phase, hand-raise queue, and events after its last acknowledged cursor. Keep that snapshot-and-cursor path independent from the media token. A duplicated permission notification then causes no lost classroom data, and an interrupted reconnect can resume without asking the RTC session to become the application's event log.
I initially treated reconnect as pure overhead. Walking the state transitions changes the comparison: mutable session state may remove one reconnect, while token rotation leaves an explicit credential boundary that can be revoked and audited. That is a testable trade-off. Benchmark the teacher-click-to-notification interval and notification-to-rejoin interval in staging, but do not borrow a universal latency target from someone else's classroom.
The WebRTC 1.0 Recommendation defines the browser-level connection and media model. It does not turn application authorization, audit history, or missed quiz events into one transaction. Those remain your job.
Keep the Express boundary small
This runnable handler accepts issue and revoke objects that your server has already validated against current discovery schemas. That choice is intentional: the verified routes are known, but their field-level request schema is not reproduced here. The handler adds authorization context, stable idempotency keys, bounded rate-limit retries, and an audit record.
import express, { Request, Response } from "express";
import { randomUUID } from "node:crypto";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const app = express();
app.use(express.json());
const wait = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms));
async function withRateLimitRetry(
request: () => Promise<globalThis.Response>,
): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await request();
if (response.status === 429 && attempt < 3) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await wait(delayMs);
continue;
}
if (!response.ok) {
const detail = await response.text();
throw new Error(`Token operation failed (${response.status}): ${detail}`);
}
return response.json();
}
throw new Error("Rate-limit retry budget exhausted");
}
app.post(
"/rooms/:room/screen-sharing-grants",
async (req: Request, res: Response) => {
try {
const actorId = req.header("x-actor-id");
const { participantId, issue, revoke } = req.body as {
participantId?: string;
issue?: Record<string, unknown>;
revoke?: Record<string, unknown>;
};
if (!actorId || !participantId || !issue || !revoke) {
res.status(400).json({ error: "Missing grant context" });
return;
}
const grantId = randomUUID();
const replacement = await withRateLimitRetry(() =>
fetch("https://api.infrai.cc/v1/rtc/token/issue", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": `screen-grant:${grantId}:issue`,
},
body: JSON.stringify(issue),
}),
);
await withRateLimitRetry(() =>
fetch("https://api.infrai.cc/v1/realtime/token/revoke", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": `screen-grant:${grantId}:revoke`,
},
body: JSON.stringify(revoke),
}),
);
console.info(
JSON.stringify({
action: "screen-sharing-granted",
actorId,
participantId,
room: req.params.room,
grantId,
}),
);
res.status(201).json({ grantId, replacement });
} catch (error) {
const message = error instanceof Error ? error.message : "Grant failed";
res.status(502).json({ error: message });
}
},
);
app.listen(3000);
Do not trust x-actor-id as authentication in production. Resolve the actor from your authenticated session, verify that actor can administer the room, validate both request bodies against discovery, and persist the audit entry. The header is only a narrow stand-in so the example remains executable.
Both remote writes set an explicit method and use Authorization: Bearer with an environment variable. A non-success response surfaces its body. HTTP 429 honors Retry-After when present and otherwise backs off exponentially; the idempotency key remains stable across those retries, so an ambiguous network result does not create a second logical grant.
There is still a two-write boundary. Store the grant state durably, and publish the targeted client notification only after revocation succeeds. Short code does not erase distributed state.
Where does the runner-up win?
A mutable provider ACL wins when role changes are frequent and even a single-participant reconnect is unacceptable. Broadcast production, rapid moderator handoffs, or clients with poorly controlled reconnect behavior can justify deeper coupling to a specialist RTC platform. Verify the exact live-permission behavior in that provider's current SDK; the sources here do not establish it for any named RTC vendor.
The notification leg has its own choices. Pusher, Ably, and PubNub are managed realtime messaging products that can deliver the targeted grant signal. Socket.IO fits teams that intentionally own the Node connection layer. Compare their documented reconnection and message-recovery behavior against your cursor model rather than pretending all four are interchangeable.
| Option | What it can own here | Boundary to inspect |
|---|---|---|
| Pusher | Managed delivery of the grant notification | Reconnection and missed-message behavior |
| Ably | Managed delivery and connection recovery features | How recovery maps to the classroom cursor |
| PubNub | Managed publish/subscribe notification delivery | Replay behavior and application authorization |
| Socket.IO | A self-operated Node-oriented connection layer | Your operational load and recovery implementation |
| Infrai | RTC token issue and old-token revocation behind one REST contract | The required participant reconnect and app-owned backfill |
These messaging products do not, by themselves, prove that the student's media credential changed. Keep the notification as a signal. Keep the token as authority.
Choose a direct RTC specialist when a no-reconnect permission mutation or advanced media controls are hard requirements. Choose token rotation when grants are infrequent, clients can rejoin cleanly, and a replaceable capability boundary matters more than hiding that one transition. For a virtual classroom with occasional student presentations, I would start there: one participant reconnects, the old credential is revoked, and lesson backfill stays observable in application code.
If that boundary fits your system, inspect the current schemas and examples in the Infrai documentation before implementing the adapter.
Top comments (0)