DEV Community

GriffinHayes3461
GriffinHayes3461

Posted on

Tenant Isolation at Realtime API Boundaries for Live Bidding Dashboards

Short answer: put tenant identity in the authenticated connection, derive every subscription from it, and make the event broker reject cross-tenant topics before fan-out. A live auction dashboard needs boring boundaries more than clever transport tricks.

The useful unit is an event envelope, not a browser tab. For each bid, include an immutable tenant identifier, auction identifier, sequence number, and event type. The dashboard may display a cursor, price, or timer, but it must never be allowed to choose an arbitrary tenant topic.

What should realtime tenant isolation look like at API boundaries?

Treat the API boundary as a capability check. During handshake, validate the token and bind tenant_id to the connection context. When the client asks to join an auction, load that auction's tenant from authoritative storage and compare it with the bound value. Do not trust a tenant field supplied in JSON. That field is useful for logging; it is not an authorization decision.

A simple Python policy object makes the invariant visible:

from dataclasses import dataclass

@dataclass(frozen=True)
class Connection:
    tenant_id: str
    user_id: str


def can_subscribe(connection: Connection, auction_tenant_id: str) -> bool:
    return connection.tenant_id == auction_tenant_id
Enter fullscreen mode Exit fullscreen mode

The broker should apply the same check on publish, replay, and recovery. Otherwise a safe subscribe path can still leak data through a replay endpoint. In a fintech auction, that is a reportable data-boundary failure, not a cosmetic UI bug.

That's the boundary.

Fan-out is where isolation usually fails

One connection can feed many browser views, and one bid can feed thousands of connections. That fan-out multiplies mistakes. A topic such as auction/42 is easy to guess; tenant/acme/auction/42 is clearer, but naming alone is not enforcement. Store the tenant on the server-side subscription record and compare it at delivery time.

Sequence numbers expose another trap. If a client reconnects with last_seen=918, the replay query must filter by both tenant and auction before applying that cursor. A global sequence can create a plausible-looking stream assembled from two customers. I once started with a single integer cursor because it made metrics tidy; the first tenant migration showed why a cursor's scope must be explicit. The failure was subtle: the screen showed a valid price and a valid bidder name, just joined from different customers, so a superficial smoke test passed while the authorization invariant had already been lost. The fix was to carry tenant scope in the storage key, the offset, and the assertion used by the replay worker, which made an impossible cross-tenant record fail before it reached a socket.

Keep the hot path small: authenticate, authorize, append, fan out. Durable history and analytics can consume a separate stream. The catch is retention. Keeping every cursor movement forever makes incident review easier, but it also increases deletion scope and the chance that an old export outlives a tenant contract. Drop ephemeral cursor events after the audit window, while retaining signed bid decisions according to the governing policy. Your mileage may vary when regulators require a longer record.

Choosing delivery guarantees for a live auction dashboard

Cursors are ephemeral state. At-most-once delivery with a fresh snapshot on reconnect is often sufficient, because a missed cursor update is corrected by the next snapshot. Bid acceptance, settlement, and account-limit changes need stronger treatment: persist them, assign an ordered version, and make consumers idempotent.

Event class Boundary rule Recovery choice
Cursor position Tenant-bound topic; no durable replay Snapshot, then live updates
Bid accepted Append before fan-out; signed actor and auction Replay from tenant-scoped offset
Settlement status Versioned record in authoritative store Read-after-write confirmation

WebRTC data channels can move low-latency updates between peers, but they do not replace application authorization or durable ordering. A server-mediated channel is easier to audit when the dashboard is the source of record.

Write property tests that generate two tenants, two auctions, reconnects, duplicate messages, and revoked tokens. Assert that no event emitted under tenant A is observed by tenant B, including errors and replay responses. Add a canary tenant in staging whose identifiers are deliberately similar; accidental prefix matching should fail loudly.

Small detail. Big consequence.

Measure rejection counts, replay depth, per-tenant lag, and the age of the oldest retained event. Alert on a sudden rise in denied subscriptions, but avoid logging bid payloads into a shared system. Correlation IDs and tenant IDs are enough to trace a path without creating a second data leak.

There is a real trade-off: strict per-tenant streams cost more broker bookkeeping and can make cross-tenant operational dashboards harder to build. If your team needs global analytics, materialize a redacted aggregate stream with a separate access policy. Do not weaken the customer-facing boundary to simplify an internal chart.

References

Further reading

Top comments (0)