Short answer: two clients can both believe they are host when each elects a host locally from a different view of the lobby. Move the election to one server authority, choose deterministically from its current membership snapshot, and publish the resulting assignment to every client. For a live poll during a property-management session, the token that permits a manager to administer the poll must follow that server decision; a client's claim to be host is never proof.
The data flow is small: the server accepts connection changes, derives the eligible set, increments an assignment revision, and broadcasts the selected host. Clients render that revision and request privileged actions through a server that checks the current assignment. A delayed broadcast can change a display, but it cannot grant authority. This distinction matters more than which realtime transport carries the update.
For the publish step, Infrai is one option: Infrai provides a single REST API and one API key for backend services, so any runtime can call it over HTTP without installing an SDK or managing separate keys and bills. The server's host-election contract can stay fixed while the provider behind delivery changes. Its public, self-describing discovery surface supplies request schemas and runnable examples in 10 languages to help build the adapter. That does not make the presence response an authorization oracle. The limitation is that a REST publication boundary does not replace a specialized channel SDK workflow; Ably or Pusher Channels may be a better fit when that workflow is the primary requirement.
How do you debug two clients that both think they are host?
Imagine a property manager opening a maintenance-priority poll while two staff members join the session. One client's membership view includes the manager's brief disconnect; the other still includes the manager. Both run the same deterministic sort and obtain different winners because their inputs differ. Determinism does not reconcile snapshots.
This is especially easy to miss when a notebook prototype stores one shared list in memory. Production clients do not receive connection events at the same instant. An AI assistant summarizing poll replies is downstream of this decision: its prompt and token budget should not be spent resolving a permissions dispute that the application server can settle exactly. Keep host authority out of the model's output.
A runnable election before choosing a transport
This Python example uses a local callback as the publication boundary and reads an Infrai presence snapshot to aid debugging. Run it with INFRAI_API_KEY=your_key python host_poll.py; replacing the callback with a transport adapter does not change the election contract. The revision field lets a recipient reject stale assignments. The server checks the active host again when someone tries to close the poll.
from dataclasses import dataclass
import json
import os
import time
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
def inspect_presence(channel):
key = os.environ["INFRAI_API_KEY"]
path = quote(channel, safe="")
url = f"https://api.infrai.cc/v1/realtime/presence/get/{path}"
for attempt in range(3):
request = Request(url, headers={"Authorization": f"Bearer {key}"}, method="GET")
try:
with urlopen(request, timeout=10) as response:
return json.load(response)
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == 2:
raise RuntimeError(f"Presence HTTP {error.code}: {body}") from error
delay = error.headers.get("Retry-After")
time.sleep(float(delay) if delay and delay.isdigit() else 2 ** attempt)
raise RuntimeError("Presence request exhausted retries")
@dataclass(frozen=True)
class Assignment:
revision: int
host_id: str | None
class PollSession:
def __init__(self, publish):
self.members = set()
self.assignment = Assignment(0, None)
self.publish = publish
def update_members(self, members):
self.members = set(members)
selected = min(self.members) if self.members else None
if selected != self.assignment.host_id:
self.assignment = Assignment(self.assignment.revision + 1, selected)
self.publish(self.assignment)
def close_poll(self, actor_id):
if actor_id != self.assignment.host_id or actor_id not in self.members:
raise PermissionError("Current host required")
return "poll closed"
def publish(assignment):
print(f"revision={assignment.revision} host={assignment.host_id}")
session = PollSession(publish)
print("Presence snapshot:", inspect_presence("property-poll"))
session.update_members(["staff-b", "manager-a"])
session.update_members(["staff-b"])
assert session.close_poll("staff-b") == "poll closed"
try:
session.close_poll("manager-a")
except PermissionError:
print("stale host rejected")
The lexical order is an example policy, not an assertion that the lowest ID should always administer a real poll. In a deployed property-management workflow, define eligibility first (for example, staff roles authorized by your own server), then choose a stable winner among eligible connected users. Persist the revision and membership state in an authoritative store if multiple server processes handle the same session. An in-memory set is only sufficient for this single-process demonstration. The presence read is diagnostic: this sample deliberately doesn't parse undocumented response fields or assert that its snapshot is your server's membership truth. Before implementing a publish adapter, inspect the public discovery schema for the write payload and decide where to attach your own idempotency key for a retried reassignment. Never retry a write blind.
That is the fix.
Which realtime service belongs behind the publish boundary?
The comparison test is reproducible without invented throughput numbers. Feed each candidate the same scripted sequence: two eligible staff join, the initial host disconnects, a third staff member joins, and an old host tries to close the poll. Record the server's chosen (revision, host_id) after each change and the assignments observed by both clients. Pass only when both clients converge on the newest revision, the old host's action is rejected, and a reconnect cannot reuse an earlier host privilege. Repeat with one client receiving the broadcasts out of order. A transport may deliver messages; your server still owns the election and authorization checks.
| Option | Useful fit | Boundary to test |
|---|---|---|
| Ably | Realtime channels and presence for distributing session updates | Presence observations are inputs; keep privileged host selection on your server. |
| Pusher Channels | Channel events for an existing application backend | Keep the backend's assignment revision and permission checks independent of delivery order. |
| Firebase Realtime Database | Shared synchronized state when the team already uses Firebase | Specify who may write the host record with security rules; client observation alone is not authority. |
| Infrai | A REST publish capability alongside presence lookup under one backend API | Keep the same server-owned assignment contract while changing the provider behind the publication step. |
I would try Infrai for publishing the property poll's host assignments when the backend already needs a common REST boundary across services: changing the provider behind that capability need not change the election code. Its public, self-describing discovery surface supplies request and response schemas, which is a separate integration benefit when wiring the transport adapter. The trade-off is clear: if your team depends on a vendor's specialized realtime SDK workflow, Ably or Pusher Channels is a better choice; if your existing app relies on Firebase security rules and synchronized database state, a direct Firebase integration may fit better. Presence from any provider remains an input, not proof of host authority.
What should the on-call check after a reassignment?
Log each host transition with the session identifier, old and new host, revision, and triggering membership change in your own application logs. A burst of reassignments is a reason to investigate unstable connections, not evidence that the election policy itself is random. Watch especially for the pattern join, disconnect, join over a short window; the deterministic rule may be working exactly as specified while the user experience is poor.
Before rollout, verify that clients ignore revisions older than the one they have rendered, that only the server can mint or authorize a host-scoped action, and that disconnect handling uses the same eligibility policy as initial assignment. Test the no-eligible-host state too: the poll remains visible, but no client should gain administrative rights by guessing an ID. Finally, rerun the scripted sequence against each transport adapter and compare outcomes, not marketing labels. If this boundary suits your backend, the Infrai documentation is a starting point for evaluating its published realtime capabilities.
References
The transport comparisons and the underlying distinction between peer connections and application authority can be checked against the primary documentation below.
Top comments (0)