Realtime Token Revocation Handling: API Edge Contracts for Live Auction Dashboard Cursors
Short answer: make revocation an explicit state transition at the API boundary, stop accepting new fan-out work immediately, and let already-authorized cursor updates expire through a short, measured lease. That gives a
Short answer: make revocation an explicit state transition at the API boundary, stop accepting new fan-out work immediately, and let already-authorized cursor updates expire through a short, measured lease. That gives a live auction dashboard a predictable view without pretending that a distributed system can provide exactly-once delivery to every browser.
This is a payments-adjacent problem even when the screen shows only bids and collaborators' cursors. A cursor is ephemeral, but its authorization is not. If a trader loses access to an auction, a stale cursor can reveal identity, inventory, or an internal review note. The system therefore needs two separate records: the durable authorization decision and the transient delivery stream.
What the boundary must guarantee
An API boundary should answer three questions in one place: is this credential currently valid, which auction and role does it authorize, and what happens to messages already queued for delivery? Checking only the WebSocket handshake is insufficient. A token can be revoked 200 ms after a connection opens, while several cursor events are waiting in a broker partition.
The practical contract is a monotonic sequence of states:
| State | New subscriptions | Queued cursor events | Audit action |
|---|---|---|---|
| active | allowed after scope check | deliver while lease is valid | append authorization id |
| revoking | reject | drain only events stamped before the revocation time | append reason and actor |
| revoked | reject | discard at the edge | append terminal decision |
| expired | reject and require re-authentication | discard | retain expiry metadata |
The revocation timestamp must come from the authorization service, not the browser clock. Every event carries the decision version (or a hash of it), the subject, the auction id, and a server sequence. Those fields make reconciliation possible when a client reconnects. They also make an audit trail that a compliance reviewer can inspect without reconstructing broker logs.
Do not call this exactly once. The useful guarantee is exactly-once effect in the consumer: a sequence number is applied at most once, and a reconnect can request the missing range. Delivery itself is at-least-once in most practical fan-out paths.
How should revoked tokens cross realtime API boundaries in an auction dashboard?
Treat each boundary as a policy enforcement point, with a narrow message contract between points. The HTTP endpoint that creates a subscription validates the token and returns a connection lease. The streaming layer validates the lease version before publishing each batch. The browser treats a revocation notice as terminal, closes its stream, and clears visible cursors before asking for a new token.
A lease is not a second credential. It is a short-lived capability bound to the original decision. In one implementation, a lease contains subject, auction, role, decision_version, issued_at, and expires_at. A revocation record includes the same identifiers plus revoked_at. Comparing versions is safer than comparing wall-clock times when authorization updates race with event publication.
Here is the core check in Go. It is deliberately independent of a particular broker or web framework:
package fanout
import "time"
type Lease struct {
Subject string
Auction string
DecisionVersion uint64
ExpiresAt time.Time
}
type Decision struct {
Version uint64
RevokedAt time.Time
}
func CanPublish(lease Lease, decision Decision, now time.Time) bool {
if now.After(lease.ExpiresAt) {
return false
}
if decision.Version > lease.DecisionVersion {
return false
}
return decision.RevokedAt.IsZero() || now.Before(decision.RevokedAt)
}
The order matters. A worker must load the newest decision before it claims a batch, then attach the decision version to the outgoing frame. If the decision changes after the claim, the edge check rejects the frame. This costs a read or a cached version comparison, but it prevents the common failure in which a queue retry resurrects a cursor from a revoked session. In a busy auction, imagine a reviewer opening the same lot in two browser tabs while an entitlement update travels through three regions: tab one claims sequence 884, the policy service advances the decision to 19, and a retry on tab two still carries version 18. Without the edge comparison, both frames look valid to the transport and only the user notices that a removed participant briefly reappeared. With it, version 18 is rejected, the client receives one terminal notice, and the audit record links the rejection to the policy change rather than to an opaque socket disconnect.
I once found a test that passed with a 500-millisecond clock skew and failed at 501 milliseconds; the error was a client-generated revoked_at. The fix was unglamorous: server timestamps, a versioned decision record, and a test that advances time in nanoseconds. Small details decide whether the dashboard is trustworthy.
Then stop.
Retention, backfill, and the cost of forgetting
The bill is usually dominated by retention and fan-out, not by the token check. Keeping every cursor event for backfill multiplies storage by the number of collaborators and by reconnect storms. A better policy is to retain a compact checkpoint per auction and a short event window, while treating cursor positions as replaceable state.
That change moves the dominant term: from unbounded event history to bounded snapshots plus a small replay window. It also changes what you can investigate. After the window expires, you may know the last accepted position but not every intermediate movement. For a payment approval or bid ledger, keep the immutable business event elsewhere; never use a cursor stream as financial evidence.
The retention table should be explicit:
| Data | Suggested purpose | What is lost when purged |
|---|---|---|
| cursor frames | immediate visual collaboration | intermediate movement |
| auction checkpoint | reconnect alignment | fine-grained path |
| authorization decisions | access and compliance audit | nothing before policy limit |
| bid and ledger events | financial truth | must not be purged on cursor policy |
The catch is operational. A short window reduces storage and privacy exposure, but a long browser outage may require a full refresh instead of a smooth replay. That is acceptable when the UI can show βre-authorize and reloadβ and the ledger remains authoritative.
Failure modes worth testing
Revocation races are the first test family. Revoke during handshake, during queue claim, during serialization, and during a reconnect. Assert that no frame stamped after the revocation decision reaches the client, even when the worker retries.
The second family is identity confusion: reuse a lease for another auction, alter a role claim, or replay an old decision version. Every case should fail closed and produce one audit record, not a stream of noisy duplicates.
The third is partial fan-out. One edge may deliver sequence 41 while another delivers 40 and 42. Clients need a gap detector and a bounded repair request. A repair response should carry the same authorization version as live frames; otherwise backfill becomes an accidental bypass.
Observability should measure decisions, not only transport health. Track revocations accepted, frames rejected at the edge, sequence gaps, lease expiry rates, and the age of the oldest retained checkpoint. Alert on a sudden rise in rejected frames: it can indicate a policy deployment, a clock problem, or a client that is not closing on terminal notices.
Choosing an implementation boundary
There are three common shapes. A managed realtime gateway centralizes connection handling and can simplify horizontal scaling, but its revocation semantics and retention controls must be verified against your policy. A self-hosted WebSocket layer gives precise ordering and storage choices, at the cost of operating connection fleets and broker capacity. Server-Sent Events are easier to inspect through ordinary HTTP tooling, yet bidirectional cursor editing needs a companion upload path and careful replay logic.
The decision should follow the guarantee you can test, not a feature checklist. Ask for a documented way to invalidate a live connection, a sequence primitive for reconnects, an authorization hook before backfill, and exportable audit records. If an option cannot expose those boundaries, it is not suitable for a regulated auction workflow, regardless of latency claims.
Stick with a simpler HTTP plus short-polling design when the dashboard has few collaborators, revocation latency is measured in seconds, or the team cannot operate a streaming fleet. Choose a persistent stream when cursor freshness materially changes a user's decision and you can fund the testing and observability described above. Your mileage may vary; the right threshold depends on auction volume, policy, and the harm of showing stale presence.
References
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.