DEV Community

LorenzHolm3752
LorenzHolm3752

Posted on

2026 Node.js Express: Per-Participant Screen-Share Publish Permission

Short answer: issue a short-lived, server-signed capability for exactly one participant, enforce it at the media gateway, and treat the live poll's event log as the source of truth for fan-out delivery. The browser can ask to share a screen, but it must never decide who may publish.

In an edtech session, a presenter may need to show a worked solution while 200 learners answer a poll. That sounds like one permission check. It is really two boundaries: authorization (who may publish) and delivery (which viewers receive the resulting track and poll state). I care about the second boundary because a perfect ACL is useless if half the room misses the question.

Invariants and failure boundaries

The authorization record should contain session_id, participant_id, capability, an expiry, and a monotonically increasing version. The server signs that record; the client presents it when negotiating a WebRTC publication. A viewer token has no publish capability, even if a user edits JavaScript in DevTools.

For the poll, persist an append-only event such as poll.opened before fan-out. Consumers acknowledge a sequence number, retry with a bounded backoff, and reconcile from the log after reconnect. At-least-once delivery is honest here: duplicates are harmless when the client applies an idempotency key, while pretending to have exactly-once delivery usually hides a lost-update path.

The hard boundary is the media gateway. It validates the signed capability on every publish attempt and checks that the participant still belongs to the session. Revoking a role updates the authorization version; a stale offer then fails closed.

That is the invariant.

How should a Node.js Express example grant screen-share publish permission to one participant?

Express is a convenient control plane, not the media plane. The route below is deliberately generic: it returns a capability that a standards-compliant WebRTC service can consume. It does not hand the browser a room-wide publish key.

from datetime import datetime, timedelta, timezone
from hashlib import sha256
import hmac
import json

SIGNING_KEY = b"replace-with-a-secret-from-key-management"

def issue_screen_share_capability(session_id, participant_id, version):
    expires_at = datetime.now(timezone.utc) + timedelta(minutes=5)
    payload = {
        "session_id": session_id,
        "participant_id": participant_id,
        "capability": "screen-share.publish",
        "version": version,
        "expires_at": expires_at.isoformat(),
    }
    encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
    signature = hmac.new(SIGNING_KEY, encoded, sha256).hexdigest()
    return {"payload": payload, "signature": signature}

# The Express handler would authenticate the instructor, load the current
# participant version, and return this object over HTTPS.
Enter fullscreen mode Exit fullscreen mode

The gateway repeats the checks instead of trusting the handler's decision. It verifies the HMAC, expiry, session membership, capability name, and version. That duplication is intentional. Control-plane state can race with an offer; the data-plane check is the last line before bytes leave the participant.

When the presenter clicks Share, the browser calls getDisplayMedia, adds the returned video track to its peer connection, and sends an offer. The server never receives raw screen pixels through the permission endpoint. It receives a negotiation request carrying the capability, then accepts only the one track type authorized by policy.

Choosing fan-out semantics for a live poll and screen track

A single WebRTC mesh is attractive for a classroom prototype, but each publisher uploads a copy to every viewer. A selective forwarding unit (SFU) keeps one upstream and forwards the track to subscribers; a relay or recording service adds another operational boundary. None of these choices fixes an unreliable event stream, so the poll log and media path need separate health signals.

Option Delivery behavior Failure mode to test Use it when
Mesh Every peer receives direct media Uploader bandwidth collapses as attendance grows A tiny room and low stakes are acceptable
SFU One upstream, server fan-out Subscriber joins after an offer and misses a track Interactive rooms need predictable uplink cost
Record-and-replay Durable artifact, delayed viewing Recording is complete but live viewers see stale state Audit or catch-up matters more than immediacy

For the poll, publish the event sequence over a reliable data channel or HTTPS subscription and include the last acknowledged sequence in reconnect requests. For screen share, renegotiation and track replacement are normal WebRTC operations; an authorization service should not infer permission from whether a track happened to exist in an earlier offer.

The failure drill I would run before launch

I start with a 200-participant load test, then remove the presenter halfway through a poll. The expected result is unglamorous: the poll remains readable, the presenter capability expires or is revoked, and viewers get a clear ended-track state. I check the timeline in five-second slices: before revocation, the gateway accepts the current version; immediately after the role change, a new offer carrying the old version is rejected; after reconnect, the client receives the poll snapshot and resumes at its acknowledged sequence. I also throttle one subscriber to force queue growth, close the presenter's tab during renegotiation, and make two identical poll acknowledgements arrive out of order. A late subscriber receives the current poll snapshot followed by new sequence numbers; it does not wait for a lucky broadcast. Those checks catch the boring bugs that dashboards hide, such as a consumer that reports healthy while silently discarding sequence 418 or a gateway that honors a cached role for one more offer.

One early design I rejected put the permission in a mutable room JSON document and let gateways cache it for a minute. That made revocation probabilistic. A participant removed at second 1 could still publish until second 60, which is unacceptable for an instructor's private answer key. The cache is valid for public metadata; it is the wrong place for an authorization decision.

Metrics should separate capability_rejected, offer_rejected, poll_ack_lag, and subscriber_reconcile. Log participant and session identifiers, never screen pixels or token material. Your mileage may vary on the expiry window: five minutes is a starting point, and the right value depends on clock skew, reconnect behavior, and how quickly instructors need revocation to take effect.

The catch is operational complexity. An SFU and durable event log cost more moving parts than a mesh and an in-memory broadcast, and this design is not suitable when the product cannot operate a media gateway or retain event history. Stick with a small mesh and ephemeral state for a classroom demo; move to signed capabilities plus replayable fan-out when missed answers, privacy, or role changes have consequences.

Keep the contract small.

References

Top comments (0)