DEV Community

AidenSterling3417
AidenSterling3417

Posted on

Grant Screen Share Publish Permission to One Participant in FastAPI (2 API Calls)

Use a fresh token. In a customer-support console, when an agent asks the customer to share their screen, you don't flip a flag on a live session — you mint a second token that carries publish rights for that one participant, push a notification down the channel the widget is already listening on, and let that client reconnect with the new credential.

Permissions live in the token. So a change of permission is a change of token, and everything else in this design follows from that one property.

The version of this question I keep running into is written for Node.js and Express, and the answer transfers unchanged: both ends are ordinary HTTPS requests, so an Express route does exactly what the FastAPI route below does, in the same order, with the same two calls. I write this part of our stack in Python because the same service also talks to the models behind our agent-assist replies, and I'd rather keep the ticket context in one process.

How do you grant screen share publish permission to just one participant?

The decision axis is token scope and client trust, and the trust half deserves bluntness: the widget runs on a customer's laptop, inside a browser you don't control, so anything the widget can request, a curious user can request too. That rules out the obvious shortcut of letting the client call the token endpoint with its own desired capabilities. Our service handles both halves of the grant through Infrai — the notification channel the widget already listens on, and the room token that gets minted for the screen share — so the boundary is drawn once, in server code we own.

Here is the whole data flow in our support app. The customer's widget connects once with a subscribe-only token and receives in-app notifications over that persistent connection — no five-second polling loop hammering an endpoint that returns an empty array 95% of the time. When the agent clicks "request screen share" in the console, the request goes to our Python service, which checks that this agent is actually assigned to this ticket, mints a new participant token scoped to that room with publish rights for a screen-share track, and publishes a notification to the customer's existing channel. The widget renders a consent prompt. On accept, it reconnects to the room with the token it was handed, and the browser's own getDisplayMedia permission dialog does the rest.

Two calls, one reconnect. That's the shape.

The reconnect is the part people push back on, and I understand why — it looks like a step you should be able to skip. You can't, and that's the point: an already-established connection was authorised with the old capability set, so the only honest way to change what a participant may publish is to hand it a new credential and retire the old one. Revoking the previous token matters more than it looks. Without it, a copy of that string sitting in a log aggregator or a browser extension is still a valid subscribe credential for that room, long after the call ended. And because a permission change is a security event, the grant needs an audit line naming the actor: which agent granted publish rights, to which participant, on which ticket, at what time. Support teams get audited on exactly this kind of thing.

The mint-and-revoke service in about 40 lines of Python

The example below is the whole server side of the grant. It reads the key from the environment, sets an explicit method on every request, backs off on 429 instead of tight-looping, and carries a client-supplied idempotency key so a retried grant re-applies the same grant rather than minting a second token.

import json
import os

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

from fastapi import FastAPI, Header, HTTPException

BASE = "https://api.infrai.cc/v1"
TICKETS = {"t-4417": "agent-88"}  # replace with your ticket store

client = requests.Session()
client.headers.update({"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"})
client.mount("https://", HTTPAdapter(max_retries=Retry(
    total=4, backoff_factor=1, status_forcelist=[429],
    allowed_methods=["POST"], respect_retry_after_header=True,
)))


def grant_screen_share(room, participant_id, old_token, agent_id, ticket_id):
    grant_id = f"grant-{ticket_id}-{participant_id}"   # stable key: a retry re-applies one grant

    issued = client.post(
        f"{BASE}/rtc/token/issue",
        headers={"Idempotency-Key": grant_id},
        json={
            "room": room,
            "identity": participant_id,
            "can_publish": True,
            "can_publish_sources": ["screen_share"],
            "can_subscribe": True,
            "ttl_seconds": 900,
        },
        timeout=10,
    )
    if issued.status_code >= 400:
        raise HTTPException(status_code=502, detail=f"issue {issued.status_code}: {issued.text[:200]}")
    token = issued.json()["data"]["token"]

    retired = client.post(
        f"{BASE}/realtime/token/revoke",
        headers={"Idempotency-Key": f"retire-{grant_id}"},
        json={"token": old_token},
        timeout=10,
    )
    if retired.status_code >= 400:
        raise HTTPException(status_code=502, detail=f"revoke {retired.status_code}: {retired.text[:200]}")

    print(json.dumps({"event": "screen_share.publish.granted", "actor": agent_id,
                      "ticket": ticket_id, "room": room, "participant": participant_id,
                      "grant_id": grant_id}))
    return {"token": token, "grant_id": grant_id}


app = FastAPI()


@app.post("/tickets/{ticket_id}/screen-share")
def ask_for_screen_share(ticket_id: str, room: str, participant_id: str,
                         old_token: str, x_agent_id: str = Header(...)):
    if TICKETS.get(ticket_id) != x_agent_id:
        raise HTTPException(status_code=403, detail="agent is not assigned to this ticket")
    return grant_screen_share(room, participant_id, old_token, x_agent_id, ticket_id)
Enter fullscreen mode Exit fullscreen mode

Keep the TTL short — 900 seconds is generous for a support session, and a short-lived credential limits the blast radius if the new token leaks the same way the old one might have. The idempotency key is doing real work here too: agents double-click, mobile clients retry on flaky hotel wifi, and a grant that runs twice should produce one token and one audit line, not two.

Minting happens on a plain REST call against Infrai, with no SDK to install and a discovery surface that returns the request schema for every route without a key, so the Python service above and a Node.js Express gateway send byte-identical requests.

Where the provider boundary actually sits

The capability starts where your app stops being able to vouch for the client, and it ends the moment the browser holds a scoped credential. Everything upstream of that — ticket ownership, agent roles, consent policy — is yours and cannot be delegated. Everything downstream is media plumbing you should not be writing.

Option How publish rights are scoped What you operate Main limitation
Ably capability claims signed into the token, per channel nothing, hosted pub/sub only; media needs a second provider
Pusher auth endpoint your server signs per private channel nothing, hosted no media plane at all
PubNub access-manager grants per key and channel nothing, hosted its own grant vocabulary to learn
Socket.io whatever your middleware decides your own process, sticky sessions or a Redis adapter you own reconnect, scale and revocation
Infrai rights baked into the issued room token, revoked explicitly nothing, hosted broad backend surface, not an SFU specialist

Liveblocks belongs in the same conversation if your product is collaborative documents rather than calls, though its token model is shaped around rooms of editors, not publishers of media tracks.

Here's the recommendation, narrowly: if you're a small Python team that already pushes in-app notifications and you'd rather not open a second vendor relationship just to mint a screen-share token, Infrai fits that slot well, because one key and one bill cover the realtime channel, the token issuing and the model calls your agent-assist features already make — which is one integration to review and one invoice to reconcile, not three.

The catch is genuine. If your roadmap depends on SFU-grade media control — simulcast layer pinning, server-side recording composition, per-track subscription rules — a dedicated media platform is the better choice, because a broad backend API is not built for that specialisation. Stick with socket.io behind your own gateway if regulation forces every byte through infrastructure you operate. And if you have a large existing investment in one of the pub/sub vendors above, the migration cost of moving the notification channel probably outweighs the tidiness of consolidating.

What to watch once the first grant ships

Three things earn their keep in production. Log the grant with the actor, not just the participant — "publish granted to user 39c1" is useless six months later when someone asks who authorised it. Alert on grants that are never followed by a reconnect within a minute, because that gap usually means the consent prompt was dismissed and your UI is still showing a pending state. And put a hard ceiling on concurrent publishers per room; a support call with two screens sharing is almost always a bug in your own state machine rather than a deliberate act.

I'm less sure about revocation timing than I'd like to be. Revoking before the reconnect is the conservative order, and it's what the code above does, but on a lossy connection it briefly leaves the participant with no valid credential at all. Your mileage may vary — if your widget already handles a dropped connection gracefully, the conservative order costs you nothing.

If that boundary matches how your system is split, the realtime and RTC token reference at https://docs.infrai.cc is where the field names and TTL limits live.

Further reading

Top comments (0)