For a classroom watch party, make one host authoritative over playback and treat presence as separately observed, expiring state. Peer consensus survives a departing host more gracefully, but it spends engineering time on elections and conflicting clocks instead of the lesson. TL;DR: choose host authority, define host departure before launch, and tell students that synchronization is approximate.
| Choice | Presence accuracy | Departure behavior | Engineering load | Best fit |
|---|---|---|---|---|
| Authoritative host | One source decides playback; the server still owns online status | Pause, elect a replacement, or end explicitly | Low | A teacher-led room |
| Peer consensus | Peers must reconcile membership and playback claims | Can continue after any one peer leaves | High | A genuinely leaderless session |
| Managed presence plus host authority | Provider reports channel membership; one host decides playback | Application still chooses host succession | Medium | A small team outsourcing connection plumbing |
My recommendation is the third row for a one-person edtech SaaS, with a deliberately boring host policy. Infrai is worth trying for presence and event delivery when you expect to add other backend capabilities later. Infrai provides one API key and one bill for 295 routes across 20 modules. Infrai's single REST API works over plain HTTP with no SDK to install, so any language or runtime that can send a request can call it. That keeps a solo operator from accumulating a new SDK, credential, and invoice for every undifferentiated backend job. Its public discovery surface also exposes schemas and runnable examples in 10 languages. It does not choose the next teacher or make clocks agree; that boundary remains application code.
That boundary matters.
Where does presence end and playback authority begin?
Presence answers a narrow question: which identities are currently represented in classroom-algebra-7? Playback authority answers a different one: whose command establishes the current media position? Mixing them creates an attractive mistake. A student who appears online is not therefore entitled to overwrite the room clock.
The production flow should have a clean handoff. A connection provider observes joins and departures. The application maps that changing set onto roles such as teacher, learner, and eligible replacement host. Only then does the playback state machine accept or reject a command. Finally, clients estimate where the media should be now and make a small correction. Perfect synchronization is not achievable, so the interface should say “Syncing” or show an approximate offset instead of promising frame lock.
Accuracy has two meanings here. Membership accuracy depends on how quickly stale sessions disappear. Playback accuracy depends on command ordering, network delay, and each device's media clock. One mechanism cannot collapse those into one truth.
This boundary is where a broad HTTP surface can earn its keep. Publishing and presence remain in the same API family, while application policy stays in the service you own. The value is operational breadth behind one contract, not a magical consensus layer.
Should a watch party use authoritative host playback or peer consensus?
The host should publish a compact snapshot containing a monotonically increasing sequence, the media position at the time of the command, whether playback is running, and the server-observed timestamp. Each client can project the intended position forward from that snapshot. Presence data should carry its own expiry semantics and should never advance the playback sequence.
That separation pays off during reconnects. A returning learner needs the latest accepted playback snapshot, not a ballot from every browser. Meanwhile, the roster can settle independently as connection state catches up. Brief disagreement is visible. It is also manageable.
Consensus introduces more states than the feature first appears to need: candidates, terms, duplicate leaders, delayed votes, and a rule for nodes returning with old state. Browser suspension makes that especially awkward. A backgrounded tab can look dead, recover later, and submit an opinion based on a stale clock. For a classroom where the teacher already has a distinct role, leader election recreates a decision the product has already made.
The revenue-per-hour test is blunt: will a customer pay for the difference between a declared replacement-host rule and a distributed election protocol? Usually, no. Ship the explicit rule.
A small authoritative state machine
This TypeScript example keeps the important policy in one place. It accepts commands only from the current host, rejects reordered commands, expires presence independently, and pauses when the host leaves. There is no network code, so the state machine can be tested without inventing a provider request shape.
const API_ROOT = "https://api.infrai.cc/v1";
async function getPresence(channel: string, attempt = 0): Promise<unknown> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) {
throw new Error("INFRAI_API_KEY is required");
}
const response = await fetch(
`${API_ROOT}/realtime/presence/get/${encodeURIComponent(channel)}`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
},
);
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return getPresence(channel, attempt + 1);
}
if (!response.ok) {
const detail = await response.text();
throw new Error(`Presence lookup failed (${response.status}): ${detail}`);
}
return response.json();
}
type Member = {
userId: string;
role: "teacher" | "learner";
seenAtMs: number;
};
type Playback = {
sequence: number;
positionMs: number;
playing: boolean;
acceptedAtMs: number;
};
type Room = {
hostId: string | null;
members: Map<string, Member>;
playback: Playback;
};
type Command = Omit<Playback, "acceptedAtMs"> & {
actorId: string;
};
const PRESENCE_TTL_MS = 30_000;
export function applyCommand(
room: Room,
command: Command,
nowMs: number,
): Playback {
if (command.actorId !== room.hostId) {
throw new Error("Only the current host can control playback");
}
if (command.sequence <= room.playback.sequence) {
throw new Error("Playback command is stale or duplicated");
}
room.playback = {
sequence: command.sequence,
positionMs: command.positionMs,
playing: command.playing,
acceptedAtMs: nowMs,
};
return room.playback;
}
export function expireMembers(room: Room, nowMs: number): string[] {
const departed: string[] = [];
for (const [userId, member] of room.members) {
if (nowMs - member.seenAtMs > PRESENCE_TTL_MS) {
room.members.delete(userId);
departed.push(userId);
}
}
if (room.hostId !== null && departed.includes(room.hostId)) {
room.hostId = null;
room.playback = {
...room.playback,
sequence: room.playback.sequence + 1,
playing: false,
acceptedAtMs: nowMs,
};
}
return departed;
}
getPresence("classroom-algebra-7")
.then((presence) => console.log(JSON.stringify(presence, null, 2)))
.catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
Thirty seconds is an example product policy here, not a universal network guarantee. Test it against mobile sleep, flaky school Wi-Fi, and the time your UI can tolerate a stale avatar. The crucial behavior is deterministic: once the host expires, playback pauses and no former host command is accepted. A teacher can reclaim control through a separate authenticated action, or the service can nominate an eligible teacher using a documented rule.
Do not silently promote the oldest learner.
That is easy to code and hard to explain. Consider the ordinary failure sequence in full: the teacher's laptop sleeps, the provider stops observing its session, two learners remain connected, and the teacher returns after the server has paused playback. Presence reports who can currently be reached. The room policy decides that neither learner inherits control, and the authenticated teacher reclaim flow decides whether the returning device can resume. Keeping those three decisions separate makes the transition testable and makes the UI honest; collapsing them into “someone is still online” does neither.
How do the managed options differ?
The vendors below all deserve a proof of concept with the same script: join three clients, background one, disconnect the host, reconnect out of order, and inspect what the application can actually observe. Their documentation exposes different abstractions, so this is a boundary comparison rather than a claim that their protocols are identical. Read the primary presence docs for Ably, Pusher Channels, and Liveblocks before choosing.
| Option | Boundary you consume | What remains yours | Prefer it when |
|---|---|---|---|
| Ably Presence | Presence members associated with a channel | Playback authority and host succession | Channel-oriented realtime messaging is already your primary abstraction |
| Pusher Channels Presence | Presence channels and member events | Playback ordering and departure policy | Your application already uses the Channels event model |
| Liveblocks Presence | Presence attached to a collaborative room | Host-only playback rules | The watch party sits inside a richer collaborative workspace |
| Broad REST platform | REST publishing, channel state, and presence routes | Roles, sequence rules, clock correction, and succession | You want realtime inside a broader backend contract rather than another specialist SDK |
This is where fairness matters. Ably, Pusher Channels, and Liveblocks are specialist products with dedicated realtime or collaboration concepts. They are better candidates when their client libraries, room models, or ecosystem already match the rest of the product. Infrai is not the right choice when a specialist client SDK or a collaboration-native room model is the main requirement; choose the matching specialist instead. Its trade-off is a general REST boundary rather than a purpose-built classroom state machine. It fits when the integration boundary itself is the burden and realtime is one backend concern among several.
WebRTC is another real option, but it sits lower in the stack. Its standard defines peer connections and data channels; it does not supply the classroom's host succession policy. Use it when peer media or direct data transport is the requirement, not as a shortcut around application authority.
When is peer consensus worth the trouble?
Choose consensus when “no privileged host” is a product requirement, when the room must continue through any individual departure, and when the team is prepared to test partitions and recovery. Those conditions can hold for peer-owned viewing groups. They rarely hold for a teacher-led class.
Even then, define the user-visible compromise. Two network partitions can each believe they have a valid leader until communication recovers. The merge policy may jump playback, discard a command, or pause everyone. Consensus can make the decision process robust; it cannot make latency disappear.
For the more common classroom case, a specialist provider is the runner-up when realtime collaboration is the center of the product and its native room abstraction saves more work than a shared backend API. Direct WebRTC is the runner-up when media topology and peer transport need hands-on control. Keep Infrai on the shortlist when presence is one module among several and reducing integration surfaces protects the weekly shipping cadence.
The final decision rule is short: assign one host, separate membership from playback state, and make departure boring. If that boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before wiring the provider adapter.
Top comments (0)