Live Auction Realtime Security: Revoked Token Boundaries, Reconnects, and Backfill
Short answer: choose a revocation contract before choosing a realtime transport. For a live auction dashboard, reject new commands as soon as revocation reaches the command boundary, stop restricted fan-out, reconnect on
Short answer: choose a revocation contract before choosing a realtime transport. For a live auction dashboard, reject new commands as soon as revocation reaches the command boundary, stop restricted fan-out, reconnect only after credential refresh, and backfill durable auction events from the last cursor the UI committed. Drop typing indicators instead of replaying them. Preserve read receipts only when they represent durable progress.
Start with this decision table. The hard choice isn't socket versus stream. It's how long stale authority may survive and which component proves that it has ended.
| Revocation contract | Pick this when | API boundary | Recovery cost |
|---|---|---|---|
| Request-only validation | The dashboard polls and every action is a separate request | Authenticate every read and command | Low; the next authorized request resumes from a cursor |
| Expiring connection lease | A short, documented revocation delay is acceptable | Validate connection creation, lease renewal, and every privileged command | Medium; refresh credentials, reconnect, then backfill |
| Push invalidation | Restricted data must stop promptly after access changes | Validate commands and publish a subject-scoped disconnect signal | High; operate invalidation delivery plus cursor recovery |
| Full snapshot on every reconnect | Event volume is small and incremental replay is unnecessary | Validate the snapshot and the new live subscription | Low client complexity; higher snapshot work |
The table forces an honest sentence into the design review: βA revoked bidder can retain this specific capability for at most this long.β If the team cannot fill in that sentence, it hasn't defined token handling yet.
What should revoked token API boundaries protect in a realtime live auction dashboard?
Protect authority, not merely connectivity. A long-lived connection says that a transport once opened. It does not prove that the user may still bid, see a restricted lot, advance a read receipt, or send a typing indicator now.
Draw five boxes in a line: credential issuer, connection gateway, auction command service, durable event log, browser projection. Then draw a side arrow from the issuer to the gateway labeled authorization version changed. That is the diagram in words. Revocation crosses the side arrow; bids cross the command boundary; accepted bid and lot events enter the durable log; the browser projection consumes that log by sequence.
Three checks follow from that picture. Connection creation checks credentials and records the subject plus the authorization version it observed. Every authority-bearing command compares that version with current authorization before it mutates auction state. Restricted outbound fan-out checks current audience membership before delivery. The second boundary matters even when the first one passed seconds earlier, because connection lifetime and authorization lifetime are different clocks.
Use status and application codes for separate jobs. At an HTTP boundary, 401 means acceptable authentication credentials were not supplied, while 403 means the authenticated subject is not allowed to perform the action. Inside an established realtime channel, a stable envelope such as AUTH_REVOKED lets the browser distinguish βrefresh onceβ from βretry later.β An application-defined close code can carry the same state, but document it as local protocol, not a Web standard.
Don't let the client guess from message text.
The most dangerous retry policy is also the easiest to write: reconnect immediately with the same revoked credential. It creates traffic without restoring authority. A safer client enters an explicit blocked state, attempts the product's normal credential refresh once, and reconnects only if that refresh establishes a new authorization version. If refresh does not restore access, the dashboard clears restricted state and returns control to the sign-in or access-request flow.
Run the revocation drill before choosing a transport
Walk one bid through a forced interleaving. At time 41, the browser holds sequence 912 and an open connection. At time 42, an administrator removes the bidder role. At time 43, the browser sends bid.place. At time 44, another bidder's accepted bid becomes sequence 913. The expected result is precise: the revoked command cannot mutate the ledger, restricted delivery stops according to the declared revocation contract, and a later authorized session can recover sequence 913 without resurrecting the rejected command.
Now vary the order. Revoke after the auction service accepts a command but before its event reaches the browser. Disconnect after the browser receives sequence 913 but before the UI commits it. Deliver sequence 913 once through backfill and once through the newly opened live channel. These aren't exotic cases. They are the minimum concurrency tests for the boundary, because each forces the team to name the authoritative fact: accepted commands are decided by the auction service; visible recovery is decided by committed sequence; duplicates are decided by event identity.
This drill exposes the transport trade-offs without turning the article into a product roundup. Polling naturally revisits authentication on every request and is a sound choice when a modest freshness delay is acceptable. A server-sent stream suits a display-heavy dashboard whose writes already use separate request APIs. A bidirectional connection fits frequent two-way interaction, but it needs command-level authorization because the connection may outlive a role. A peer data channel adds a peer-to-peer connection model; the W3C WebRTC recommendation defines that transport context, but it does not replace the centrally authoritative auction ledger or the application's revocation contract.
Pick the smallest mechanism that passes the drill.
For a B2B SaaS auction workspace, classify collaboration signals in the same exercise. A typing indicator is ephemeral: give it a short lifetime, never backfill it, and wait for a new signal after reconnect. A read receipt can be durable when it advances a monotonic βseen through sequenceβ position. It should never move backward. Accepted bids and lot-state transitions are durable because the reconstructed dashboard depends on them.
| Event class | Store for replay? | Behavior after reconnect | Invariant to test |
|---|---|---|---|
| Accepted bid | Yes | Apply after the committed cursor | Event ID is idempotent and sequence is ordered |
| Lot transition | Yes | Apply in sequence | Older state cannot replace newer state |
| Read receipt | Usually | Resume its highest committed position | Position never regresses |
| Typing indicator | No | Wait for a fresh signal | Stale typing never reappears |
| Viewer presence | No | Recompute from current heartbeats | Old presence is not replayed |
βUsuallyβ is deliberate. A read receipt used only as a disposable animation does not need durable storage. A receipt used for an audit-visible acknowledgement does. I'm not sure a universal retention interval exists for either auction events or receipts; event rate, audit obligations, and the promised offline window determine it. Write those inputs into the service contract instead of choosing a round number by habit.
Implement recovery around committed state
Reconnect and backfill form one state machine: live becomes refreshing, then subscribing, then backfilling, and finally live again. Events arriving on the new subscription wait in a bounded buffer until backfill finishes. The browser applies only durable events whose sequence is newer than the snapshot watermark, deduplicates by event ID, sorts by sequence, and advances its committed cursor after rendering succeeds.
The order is important. Subscribe first and obtain a server watermark, fetch a snapshot at that watermark, request durable events after it, merge the live buffer, then publish the reconstructed view. If a cursor is outside the service's retention policy, request a fresh snapshot rather than retrying an impossible incremental range. Exactly-once transport is unnecessary when event application is idempotent and the committed cursor is explicit.
The following TypeScript keeps network details behind generic interfaces. That makes the boundary visible without pretending that endpoint names or close codes are standardized.
type DurableEvent = {
eventId: string;
sequence: number;
kind: "bid.accepted" | "lot.changed" | "receipt.advanced";
payload: Readonly<Record<string, unknown>>;
};
type Snapshot = {
watermark: number;
lots: ReadonlyArray<Readonly<Record<string, unknown>>>;
};
type RecoverySource = {
subscribe(accessToken: string): Promise<{
watermark: number;
buffered: DurableEvent[];
}>;
snapshot(accessToken: string, watermark: number): Promise<Snapshot>;
eventsAfter(accessToken: string, sequence: number): Promise<DurableEvent[]>;
};
type Projection = {
replace(snapshot: Snapshot): void;
apply(event: DurableEvent): void;
commit(sequence: number): void;
};
async function recover(
source: RecoverySource,
projection: Projection,
accessToken: string,
): Promise<void> {
const subscription = await source.subscribe(accessToken);
const snapshot = await source.snapshot(accessToken, subscription.watermark);
const backfill = await source.eventsAfter(accessToken, snapshot.watermark);
const byId = new Map<string, DurableEvent>();
for (const event of [...backfill, ...subscription.buffered]) {
if (event.sequence > snapshot.watermark) byId.set(event.eventId, event);
}
const ordered = [...byId.values()].sort(
(left, right) => left.sequence - right.sequence,
);
projection.replace(snapshot);
for (const event of ordered) {
projection.apply(event);
projection.commit(event.sequence);
}
}
Keep authentication failure outside this function's retry loop. The caller owns one credential refresh attempt and the transition to blocked; recovery owns ordering only. That separation prevents a security decision from being buried inside data reconciliation.
Observability should describe the state machine, not celebrate an open socket. Count revocation decisions at the connection, command, and fan-out boundaries. Count refresh outcomes, reconnect attempts, cursor-expired snapshots, duplicate event IDs, and sequence gaps. Measure reconnect-to-live duration and newest-committed-event age. Break those signals down by auction and client version, but keep subject identifiers out of low-control metric labels.
A dashboard can be connected and wrong.
Alert on violated invariants rather than ordinary churn. A sequence gap that remains after backfill, a command accepted under an obsolete authorization version, or a projection whose event age exceeds the product's freshness objective deserves attention. Routine reconnects do not, unless their rate or recovery time crosses a team-owned threshold. In deployment, release the new recovery state machine behind a cohort flag, compare gap and duplicate counters, and retain the previous client protocol until the new cohort demonstrates compatible cursors.
Pick this when the operational bill is justified
Choose request-only validation and polling when auction updates tolerate delay, connection operations would overwhelm a small team, or full snapshots are cheap. Choose an expiring connection lease when a bounded authorization delay is acceptable and the team can state that bound to customers. Add push invalidation when continued restricted delivery after revocation carries enough risk to justify operating an invalidation path.
For recovery, choose full snapshots when state is compact. Choose cursor backfill when event history is large, disconnects are common, or preserving a precise accepted-bid timeline matters. The catch is that cursor backfill adds retention, watermark, buffering, idempotency, and gap-detection duties. It is not suitable when the service cannot guarantee a stable order or the client cannot bound its live buffer; stick with an authorized fresh snapshot in that case.
WebRTC data channels are a poor default for a centrally governed auction dashboard because peer connectivity adds signaling and application-level recovery work while the auction still needs one authoritative ledger. They can fit a genuinely peer-oriented feature, but that is a different decision from revocation enforcement.
Limits and handoff criteria
No client state machine can repair an ambiguous server contract. Before launch, write down the maximum revocation propagation time, which commands require current authorization, which events are durable, how long cursors remain valid, what a snapshot watermark means, and which metric proves the browser is current. Security, backend, frontend, and on-call owners should agree on those six items.
The full lease-plus-invalidation-plus-backfill design costs more to build and operate. Don't use it for a dashboard that can honestly refresh every few seconds from a compact snapshot. Do use it when stale authority and missing accepted bids are unacceptable, then test the ugly interleavings before production traffic does it for you.
The final decision is compact: revocation ends authority at commands and restricted fan-out; reconnect requires refreshed credentials; durable auction facts backfill from committed state; ephemeral collaboration signals expire. Everything else is a transport choice.
References
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.