Choose an authoritative host for watch party playback synchronisation unless playback must continue through host departure without a server taking over. One source of truth removes almost all of the coordination machinery. Peer consensus survives more kinds of departure, but it also makes ordering, membership, and conflict resolution part of your product.
TL;DR: broadcast the host's playback intent with an increasing sequence number, reject old or duplicate events, and make host departure an explicit state transition. Perfect synchronisation is not achievable. Say so in the UI.
For a Node.js watch party, the transport is secondary to that ownership rule. The implementation below is Go because the operational invariant is language-independent: one accepted timeline, one epoch, and an idempotent retry path.
Should watch party playback use an authoritative host or peer consensus?
Use the authoritative host by default. A host emits play, pause, and seek intent; every viewer accepts only the newest event in the current host epoch. Clients can correct their local players toward that position, but they should not vote on the current timestamp. Network delay and media buffering guarantee visible skew, so the interface should show reconnecting or out-of-sync status rather than promise frame-perfect playback.
Peer consensus earns its complexity only when losing the host must not interrupt control. That requirement brings membership changes, leader election or quorum rules, partition behavior, duplicate suppression, and a deterministic conflict policy. Imagine host A seeks to 18:04 just before a partition while a newly elected host B pauses at 17:59. “Use the latest event” is not a policy until the system defines whose clock and which epoch establish latest. A small feature has become a distributed log.
After being paged for missed jobs and duplicate deliveries, I treat that ambiguity as an incident waiting for a trigger. The same idempotency reflex applies here: an event can arrive twice, late, or after ownership changed. Design those cases before tuning animation.
Keep it boring.
Four products can sit around this decision, but a realtime product does not choose the authority model for you. Ably and Pusher Channels are candidates when their channel and presence client models fit an application already built around persistent connections. Liveblocks is the more natural candidate when the watch experience is part of a broader collaborative room with shared application state. Infrai fits a team that wants one REST API with no SDK to install and one key for 295 routes across 20 modules; its public discovery endpoint supplies schemas, billing data, and runnable examples, while one bill reduces reconciliation work when the watch service also uses adjacent backend capabilities.
| Candidate | Integration emphasis | Best fit here | Main limitation to evaluate |
|---|---|---|---|
| Ably | Realtime client and presence model | Persistent channel clients | Application still owns epochs and ordering |
| Pusher Channels | Channel-oriented client model | Existing channel-based applications | Application still owns failover policy |
| Liveblocks | Collaborative room state | A watch room with broader shared state | Separate durable state from cursor-like presence |
| REST capability service | Plain HTTP plus discovery | Services avoiding another SDK | Rich local collaboration remains application work |
There are real limits. The REST option is not a fit when the team wants a vendor-specific client SDK to own rich local collaboration state, and the verified facts here do not establish measured latency or uptime. Evaluate the three client-oriented products against the actual connection and recovery requirements. More importantly, choose peer consensus instead of any single-authority design when uninterrupted decentralized control is a hard product requirement and the team accepts the larger correctness surface.
Implement the authority and inspect presence
The safe implementation has two layers. The application owns the playback epoch and sequence rule. The realtime service carries events and reports presence. Do not infer a publish body from an endpoint description; retrieve its discovery schema and use the runnable example before wiring a writer.
This minimal Go program calls the verified presence route. It uses an environment key, an explicit method, URL-escapes the channel, checks every response, and retries HTTP 429 with Retry-After or exponential backoff. It prints raw JSON because no presence response shape was supplied here; inventing a struct would turn an example into a guess.
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(response *http.Response, attempt int) time.Duration {
if seconds, err := strconv.Atoi(response.Header.Get("Retry-After")); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
baseURL := strings.TrimRight(os.Getenv("REALTIME_BASE_URL"), "/")
if baseURL == "" {
panic("REALTIME_BASE_URL is required")
}
channel := "watch-party-room-42"
pathTemplate := "/v1/realtime/presence/get/{channel}"
path := strings.Replace(pathTemplate, "{channel}", url.PathEscape(channel), 1)
endpoint := baseURL + path
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
request, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
panic(err)
}
request.Header.Set("Authorization", "Bearer "+key)
response, err := client.Do(request)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(response.Body)
response.Body.Close()
if readErr != nil {
panic(readErr)
}
if response.StatusCode == http.StatusTooManyRequests {
time.Sleep(retryDelay(response, attempt))
continue
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
panic(fmt.Sprintf("presence request failed: status=%d body=%s",
response.StatusCode, strings.TrimSpace(string(body))))
}
fmt.Println(string(body))
return
}
panic("presence request remained rate limited after four attempts")
}
Presence does not prove synchronisation. It answers who the service currently sees in the channel; the application still needs a playback message containing a host epoch, an increasing sequence, the intended state, and a media position. The exact publish fields must come from live discovery. For a write retry, send the platform's Idempotency-Key header so a repeated request cannot double-apply; the platform convention has a 24-hour default deduplication window.
Do not let a viewer-provided wall clock order events. Server acceptance establishes order inside an epoch, while a new host starts a new epoch. That single boundary prevents a packet from the departed host from overriding the replacement.
Handle host departure before launch
Write the departure runbook while the happy path still feels obvious. When the authoritative host disappears, freeze new control actions, mark synchronisation as uncertain, select a replacement only under the documented policy, increment the epoch, and require every viewer to rejoin that epoch. Playback may continue locally during the pause, but the UI must stop claiming that the room is synchronized. Once authority is restored, issue one explicit state snapshot rather than replaying every speculative click made during the gap.
A server authority changes the trigger, not the rule. If a browser host leaves, another host must be chosen. If a server authority becomes unreachable, clients enter the same uncertain state and reconnect. Pretending either event cannot happen just moves the decision into an outage.
Rollback should be dull: disable synchronized controls, leave local playback available, and preserve the event trail. The watch party is optional. Playback is not.
Verify ordering, recovery, and honest UI
Test state transitions instead of watching two laptops play the same scene. Deliver sequence 12 before sequence 11 and confirm the player never seeks backward. Deliver sequence 12 twice and confirm the retry changes nothing. Reconnect the host with a fresh epoch, then deliver an old-epoch pause. Remove the host during a seek and verify every viewer enters an uncertain state before accepting a replacement snapshot. Partition two candidate hosts, restore connectivity, and confirm that only the selected epoch can issue control events.
Use at least three views during verification: the authoritative event record, per-client accepted sequence and epoch, and current channel presence. Log duplicate rejection, stale-epoch rejection, host departure, election start, and authority restoration with a correlation ID. Alert on sustained rejection or reconnection rates instead of paging on one dropped viewer. A short disconnect is routine; a room that repeatedly changes authority is operationally meaningful.
Then test the wording. Artificially delay a viewer and confirm that it says “catching up,” not “synchronized.” A precise label is part of the failure model.
The final decision is intentionally uneven: authoritative host wins for ordinary watch party synchronisation; peer consensus wins only when decentralized continuity is worth owning elections and conflicts. Transport choice follows from the connection model, recovery behavior, and integration constraints. It cannot rescue an undefined authority policy.
Top comments (0)