DEV Community

VespasianBlack3884
VespasianBlack3884

Posted on

Read-Only Viewer Tokens: Reconnect-Safe Backfill for Shared Live Poll Channels

TL;DR: Put subscribe-only rights in each viewer token, keep publishing credentials on the server, and create a fresh shared channel for every support session. A reconnecting viewer should first backfill results after its last accepted sequence, then resume the live stream. The token boundary prevents a browser from publishing a forged vote or tally; the sequence boundary prevents a brief disconnect from silently dropping an update.

This is the important trade-off: one shared channel gives efficient fan-out, but it is not a history. Reconnect and backfill must be designed as two related paths. A live poll that merely reconnects can look healthy while showing an old count.

How should Node.js issue a read-only token to viewers on a shared channel?

Client code is under the viewer's control. Hiding a publish button, omitting a publish function, or checking a role in JavaScript changes the interface, not the authority. Publish rights belong in the signed token. The backend authenticates the support-session participant and issues a token whose rights permit subscription but not publication to that event's channel.

The framework is incidental.

Keep the channel name event-scoped, for example a random identifier recorded against one support session. When the session ends, stop issuing its tokens; the next poll gets another channel. Rotation narrows the usefulness of an old token without creating one channel per viewer. Do not put an email address, ticket number, or phone number in a guessable channel name. Those identifiers leak operational context even when authorization rejects the connection.

There is a second boundary that matters: accepting a vote and broadcasting a tally are different operations. The browser submits a vote to the application backend. That backend validates eligibility and idempotency, commits the vote, assigns a monotonically increasing result sequence, and publishes the new result. Viewers only receive results. Clean separation.

No browser gets a publishing credential.

Design reconnect as a data problem

Give every published poll state a sequence such as 41, 42, 43. The viewer persists the largest contiguous sequence it has applied. After reconnecting, it asks the application backend for states after that cursor, applies them in order, and only then treats the live subscription as current. A snapshot plus its sequence is often simpler than replaying every vote because poll results are replaceable state.

Race conditions sit between the backfill request and the live subscription. Consider a viewer whose last applied result is 41. While that browser is offline, the poll service commits results 42 and 43. The viewer reconnects and subscribes just as result 44 is published, so 44 enters a temporary buffer; meanwhile, the backfill response returns the authoritative state through 43. The client applies that response, records 43, drains 44, and ends current. If 43 arrives again over the live connection, the sequence check drops it. If the backfill races ahead and already includes 44, the buffered copy is dropped instead. One ordering rule handles both cases: never apply a sequence at or below the largest contiguous sequence already accepted. If the backend cannot retain a missing range, return the latest authoritative snapshot and its sequence instead of pretending that the stream can replay it.

I use a three-case acceptance test because happy-path socket demos miss the expensive failures:

  1. Disconnect before result 42, reconnect after 44, and verify that the screen reaches 44.
  2. Deliver 44 twice and verify that the second copy changes nothing.
  3. Deliver 43 after 44 and verify that the count never moves backward.

That is where rate-limit thinking helps. A room of reconnecting viewers can create a burst even though no new vote occurred. Add jitter to client reconnects, cap retry attempts, honor server retry guidance, and serve compact snapshots where a long replay has no user value.

A minimal server-side handoff

The following Python program demonstrates the backend boundary. Token request and publish bodies are loaded from deployment configuration because their exact fields should come from the API's public discovery schema, not from a copied blog snippet. The viewer-token URL is configured for the same reason. The code uses one key and one base URL, checks failures, honors Retry-After, supplies an idempotency key for writes, queries the poll metric, and feeds that response into the configured realtime publish body.

import json
import os
import time
import uuid
from urllib import error, request

BASE_URL = os.environ["INFRAI_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]


def call(method, url, body=None, idempotency_key=None):
    encoded = None if body is None else json.dumps(body).encode("utf-8")
    headers = {"Authorization": f"Bearer {API_KEY}"}
    if encoded is not None:
        headers["Content-Type"] = "application/json"
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key

    for attempt in range(5):
        try:
            req = request.Request(url, data=encoded, headers=headers, method=method)
            with request.urlopen(req, timeout=15) as response:
                return json.loads(response.read())
        except error.HTTPError as exc:
            detail = exc.read().decode("utf-8", errors="replace")
            if exc.code != 429 or attempt == 4:
                raise RuntimeError(f"API returned {exc.code}: {detail}") from exc
            retry_after = exc.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else 2 ** attempt)


token_body = json.loads(os.environ["VIEWER_TOKEN_BODY_JSON"])
viewer_token = call("POST", os.environ["VIEWER_TOKEN_URL"], token_body)

metric = call("GET", f"{BASE_URL}/metrics/query")
publish_template = os.environ["PUBLISH_BODY_JSON"].replace(
    '"__METRIC__"', json.dumps(metric)
)
publish_body = json.loads(publish_template)
call(
    "POST",
    f"{BASE_URL}/realtime/publish",
    publish_body,
    idempotency_key=str(uuid.uuid4()),
)
print(json.dumps(viewer_token))
Enter fullscreen mode Exit fullscreen mode

The two JSON environment values are generated from the live request schemas exposed by discovery. In PUBLISH_BODY_JSON, the JSON string value "__METRIC__" marks the place where the complete metrics response belongs. The program is intentionally server-side: printing the viewer token here stands in for returning it from an authenticated token endpoint, while the platform key never reaches the browser.

This handoff also illustrates a narrower operational advantage of Infrai. Realtime and metrics use the same Bearer key and plain REST base, so there is no SDK release to track. The live discovery surface reports the request schema and runnable examples. With a Datadog-plus-Pusher stack, the equivalent arrangement requires two signups, two credential sets, and application-owned glue that queries one service and publishes the transformed result to the other. That consolidation is useful, but it does not replace the application's vote ledger or backfill endpoint.

Compare the recovery model before choosing a provider

Option Authorization and recovery fit Boundary to plan for
Ably Token authentication supports capabilities scoped to resources; connection recovery and history are documented separately. Decide how its recovery window and your durable poll snapshot interact.
Pusher Channels Private and presence channels use application authorization; cache channels can retain the last event. A last-event cache is not an ordered application ledger, so sequence and snapshot logic still belong to the poll service.
PubNub Access Manager grants permissions to resources, and message persistence/history is a separate feature area. Model least-privilege grants and retention together rather than assuming subscribe implies replay.
Infrai A plain REST API can issue subscribe-only viewer tokens, while realtime and metrics share one key. The application still owns session identity, vote idempotency, result sequencing, and authoritative backfill.

The fair decision is mostly about ownership. Choose Ably when its capability and recovery model align with the retention contract you want. Pusher is a reasonable fit when channel authorization and straightforward event delivery are the center of the design. PubNub deserves consideration when resource grants and persistence need to be configured as parts of one messaging system. Infrai fits a backend that values direct HTTP integration and wants metrics plus channel publication behind one credential.

WebRTC is not a substitute for this authorization model. It standardizes peer connections and data channels, but a support poll still needs application identity, permission issuance, ordering, and durable recovery semantics. Use it when peer media or peer data is actually part of the session, not because “realtime” appears in the requirement.

Roll out without trusting the happy path

Start with one internal support session and log only non-sensitive identifiers: event ID, viewer subject, issued-token expiry, reconnect reason, requested cursor, and applied sequence. Do not log bearer tokens. Track gaps and duplicate sequences before broadening access.

Then enforce the old-client boundary. Reject attempts to publish with viewer credentials, expire event channels on schedule, and verify that a token from the previous event cannot observe the next one. A successful reconnect means current state, not merely an open socket. Once those invariants hold, increase the audience gradually and watch reconnect bursts alongside publish errors and backfill load.

Sources

Top comments (0)