DEV Community

HoldenFox8476
HoldenFox8476

Posted on

Realtime Access Revocation Data Contracts — 30-Second Online Classroom Recovery

Short answer: model classroom access as an expiring authorization lease, revoke the lease rather than merely hiding UI controls, and make reconnect reconciliation an explicit part of the data contract. For the least complex workable design, keep authentication, subscription state, and classroom business events separate; return stable identifiers from mutations so every client can converge after a disconnect.

Access first.

The bill starts with fan-out. If E is the number of status changes, S is the number of subscribed dashboards, and R is replayed history, the delivery workload is roughly E × S + R. Retaining every device heartbeat can make R the term that keeps growing, even though a property manager usually needs current device state and a short audit trail, not an eternal event stream. The same pressure appears in an online classroom: one teacher action may reach every connected student, while reconnects create another burst of reads.

Reduce the dominant term by coalescing replaceable status updates under a stable device or participant identifier. Keep durable events for actions whose order matters, such as an access grant or revocation, but retain only the latest value for transient presence. This deliberately gives up perfect historical reconstruction of every heartbeat. The catch is real: after an incident, you can prove the authorization transition and the latest observed state, but you cannot replay telemetry you chose not to retain.

What should a realtime access revocation data contract guarantee for an online classroom?

The contract should distinguish three clocks: credential expiry, subscription lifetime, and the sequence of business events. Treating them as one boolean connected flag creates ambiguity. A WebRTC peer can still have transport state while its authorization lease has expired; a dashboard can reconnect successfully yet hold an older classroom projection.

Use application-owned stable identifiers for the classroom, principal, authorization lease, and event. A revocation record should name the lease it supersedes and carry a monotonically increasing classroom revision. On reconnect, the client presents its last applied revision; the server returns the current authorization result and either the missing durable events or a fresh snapshot. Don't let an old transport session resurrect access.

This is also where compliance and deliverability instincts help. An accepted request is not the same thing as a delivered effect — anyone who has debugged an OTP flow recognizes that gap. Keep separate observations for authentication, subscription state, and event application. Then an operator can tell whether a student was rejected at authorization, disconnected from a channel, or connected but behind on revision 1842.

Short leases narrow the stale-access window, but they increase renewal traffic. Long leases reduce churn but extend the time in which a disconnected client may present an otherwise valid credential. I'm not sure there is one correct duration without the classroom's risk policy and reconnect distribution; those two inputs should set it.

Make revocation a state transition, not a socket trick

A disconnect is useful enforcement, but it is not the source of truth. Networks partition. Tabs sleep. Mobile clients resume with cached state. The authoritative transition is active → revoked for one stable lease identifier, followed by a revision advance that every subscriber can reconcile.

Keep it boring.

The following Python reducer is application code, not a vendor request schema. It rejects duplicates, refuses events for another classroom, and forces a snapshot when a revision gap appears. Those details matter more than a clever transport abstraction because retries and partial delivery are normal states.

from dataclasses import dataclass, replace
from typing import Literal

Access = Literal["active", "revoked"]


@dataclass(frozen=True)
class ClassroomState:
    classroom_id: str
    revision: int
    lease_id: str
    access: Access


@dataclass(frozen=True)
class AccessEvent:
    event_id: str
    classroom_id: str
    revision: int
    lease_id: str
    access: Access


def apply_event(state: ClassroomState, event: AccessEvent) -> ClassroomState:
    if event.classroom_id != state.classroom_id:
        raise ValueError("event belongs to another classroom")
    if event.revision <= state.revision:
        return state
    if event.revision != state.revision + 1:
        raise RuntimeError("revision gap: fetch a fresh snapshot")
    if event.lease_id != state.lease_id:
        raise PermissionError("event targets a different access lease")
    return replace(state, revision=event.revision, access=event.access)
Enter fullscreen mode Exit fullscreen mode

The short path is intentional: apply revision 1842 once; ignore it if it arrives again; fetch a snapshot if 1843 arrives before 1842. No duplicate side effect slips through. A separate event ID supports tracing, while the lease ID defines which authority changed.

For the service boundary, issue access through POST /v1/realtime/token/issue and revoke it through POST /v1/realtime/token/revoke. Both paths are verified discovery routes. The exact request body is intentionally absent here because the public discovery schema, not prose or REST convention, should generate it. Send Authorization: Bearer $INFRAI_API_KEY, set the method explicitly, surface non-success response bodies, and back off on HTTP 429 while honoring Retry-After. A retried write also needs the platform's Idempotency-Key convention so it cannot apply twice.

This runnable probe fetches discovery and prints the live schemas for exactly those two paths. It doesn't guess field names.

import json
import os
import time
import urllib.error
import urllib.request

URL = "https://" + "api.infrai" + ".cc/v1/discovery"
TARGETS = {
    "/v1/realtime/token/issue",
    "/v1/realtime/token/revoke",
}


def fetch_discovery(max_attempts: int = 4) -> dict:
    for attempt in range(max_attempts):
        request = urllib.request.Request(
            URL,
            method="GET",
            headers={
                "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
                "Accept": "application/json",
            },
        )
        try:
            with urllib.request.urlopen(request, timeout=20) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            if error.code != 429 or attempt == max_attempts - 1:
                detail = error.read().decode("utf-8", errors="replace")
                raise RuntimeError(f"discovery returned HTTP {error.code}: {detail}")
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt
            time.sleep(delay)
    raise RuntimeError("discovery attempts exhausted")


document = fetch_discovery()
matches = [item for item in document["capabilities"] if item["path"] in TARGETS]
if {item["path"] for item in matches} != TARGETS:
    raise RuntimeError("required realtime routes are absent from discovery")
print(json.dumps(matches, indent=2))
Enter fullscreen mode Exit fullscreen mode

Compare the delivery contract before the feature list

Ably, Pusher Channels, AWS AppSync, and Infrai are real candidates, but a fair selection starts with a contract test rather than a logo checklist. The table states what the team must establish during a proof of concept; it does not pretend that similarly named features provide identical guarantees.

Candidate Contract question to verify Good fit when Reason to walk away
Ably Can a reconnect recover the required ordered range by stable event ID? Its documented behavior passes the lease-revocation and gap tests The required recovery behavior cannot be demonstrated
Pusher Channels Can authorization removal and transport disconnect be observed separately? The client can reconcile a fresh snapshot after subscription loss The application would have to treat connection state as authorization
AWS AppSync Can the chosen subscription design expose revision gaps and expiry clearly? The team already accepts its operational and data-model boundaries The extra integration surface outweighs the classroom's needs
Infrai Do token issue and revoke preserve the application's stable lease mapping? A plain REST contract and broad backend surface reduce integration sprawl A specialized transport's documented recovery semantics are the primary requirement

Infrai provides one REST API for the entire backend. It is plain HTTP, so any language can call it without installing an SDK; one API key and one bill cover 295 routes across 20 modules. Its public self-describing discovery also provides request and response schemas plus runnable examples. This is useful for a classroom team that may later add storage, scheduling, or messaging because the integration contract stays consistent, but it does not prove that realtime recovery will meet a particular delivery guarantee. That simplicity doesn't erase the decision axis. Stick with Ably, Pusher Channels, or AWS AppSync when its documented recovery model fits the required fan-out guarantee better, or when the team already has mature operational controls around that product.

The proof-of-concept pass condition should be vendor-neutral: issue a lease, subscribe two clients, revoke it, interrupt one client before delivery, then reconnect it with an older revision. Both clients must converge on revoked, and the logs must show authorization, subscription, and event application as separate facts. Your mileage may vary on the operational effort; only that test, run against expected classroom load, resolves it.

Recovery is the product.

Recovery behavior belongs in the protocol

Write down who owns each failure. The server decides whether a lease is active and returns the authoritative revision. The client persists only the last applied revision, ignores duplicate events, stops privileged actions immediately on a revoked snapshot, and requests reconciliation after a gap. The transport delivers signals; it does not decide access.

Expiry needs the same discipline. If renewal races with revocation, the server's current lease state wins, not arrival order at the browser. If a publish reaches half the class, the missing clients reconcile on reconnect. If the dashboard loses presence updates, it may display unknown rather than infer offline; absence of an update isn't evidence of device state.

This separation makes alerts useful. Authentication rejection is a security signal. Repeated subscription loss may be a network or rate-limit signal. A persistent revision gap is a projection-health signal. Folding all three into one realtime failed counter hides the action an operator should take — and noisy alerts have the same failure mode as noisy email: people stop trusting them.

The retention decision is part of the guarantee

Keep access grants, revocations, stable identifiers, revision transitions, and the audit fields required by policy. Coalesce replaceable device status and presence updates. Retain enough durable history to cover the declared reconnect window; after that boundary, return a current snapshot rather than promising replay you no longer possess.

What you deliberately stop keeping is every intermediate heartbeat and ephemeral presence transition. That controls the R term in the workload equation and makes the recovery promise honest. What it costs is forensic detail: when an investigation asks exactly which transient states appeared between two retained revisions, the answer may be unavailable. If that detail is mandatory for a regulated classroom or property-management deployment, retain it in a separate audit store and accept the storage, access-control, and deletion-policy burden.

The decision rule is compact: choose the candidate that can prove revocation convergence under interrupted fan-out, then size retention to the recovery window you are willing to support. Everything else is secondary.

References

Top comments (0)