Short answer: make presence a tested, expiring claim and attach every bidder notification to a versioned authorization envelope. For a live auction dashboard, this gives the server a defensible answer to "may this bidder see this event now?" instead of treating an open WebRTC channel as permission.
The useful unit is a decision record, not a socket. It should survive reconnects, replay, and a late disconnect without changing meaning.
What must a realtime bidder contract prove before delivery?
Start with five signals: authenticated audience, auction membership, session version, expiry, and sequence. The notification body says what happened; the envelope says why this recipient can receive it and where it belongs in the stream. A client can display the body, but it must not be able to edit the envelope and widen its audience.
Use opaque IDs. Give each event an immutable ID for deduplication, a bidder ID, an auction ID, a schema version, a policy version, a session version, and a monotonic sequence in a documented scope. Issue and expiry times make freshness explicit. Policy version and session version answer different incident questions: which rule admitted the event, and which connection lease was current?
Here is a small Go contract for a dashboard notification. Amounts are integers in minor currency units, so encoding does not depend on floating-point behavior.
package notification
import "time"
type Envelope struct {
EventID string `json:"event_id"`
SchemaVersion uint32 `json:"schema_version"`
PolicyVersion string `json:"policy_version"`
AuctionID string `json:"auction_id"`
BidderID string `json:"bidder_id"`
SessionVersion uint64 `json:"session_version"`
Sequence uint64 `json:"sequence"`
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
Body BidNotice `json:"body"`
}
type BidNotice struct {
Kind string `json:"kind"`
AmountMinor int64 `json:"amount_minor"`
Currency string `json:"currency"`
}
An envelope is evidence of a decision, not a reusable credential. Never put an access token, another bidder's private bid, or an internal risk score in it. Build an audience-specific body only after the server identifies the authenticated subject and checks current membership.
How do leases and sequence numbers keep presence accurate?
Transport state is a noisy presence signal. A laptop can sleep without a clean close, a reconnect can overtake a disconnect, and membership can change while a channel remains open. WebRTC carries application data, but its transport lifecycle is not the auction’s authorization source of truth.
Model each connection as a renewable lease with a session version. A heartbeat renews it. A disconnect retires it only when its session version still matches the current one. That comparison is the guard against an old close erasing a newer connection.
I have been paged by missed jobs and duplicate deliveries in queue infrastructure. The transferable habit is simple: every transition gets an identity, and every consumer has a stale-work rule. In this path, (bidder_id, session_version, sequence) supplies that rule without pretending delivery is exactly once.
A deterministic fixture makes the edge case concrete. Session 41 connects, session 42 replaces it, then a delayed close for 41 arrives. The reducer ignores that close. Duplicate sequence 816 twice and deliver 815 afterward; the client applies 816 once, ignores the duplicate and stale event, and records the reason.
Order wins.
I'm not sure what lease duration fits your bidder population. A fixed value without observed mobile and desktop reconnect distributions is guesswork. Choose it from latency data, document the grace rule, and test both false-online and false-offline outcomes.
Can one authorization boundary serve delivery and replay?
Yes, if the check is deterministic and its changing inputs are explicit. Keep network and cache access outside the function so a replay test can reconstruct the same result. The fan-out path and the reconnect path should call the same decision boundary.
| Signal | Input | Reject when |
|---|---|---|
| Audience | Authenticated bidder and bidder_id
|
Identities differ |
| Membership | Current auction membership and auction_id
|
Scope differs or membership is inactive |
| Freshness |
expires_at and evaluation time |
The envelope has expired |
| Session | Current lease version | The connection is older than the lease |
| Order | Last accepted and incoming sequence
|
The event is duplicate or stale |
package notification
import "errors"
var (
ErrWrongAudience = errors.New("wrong audience")
ErrNotMember = errors.New("not an auction member")
ErrExpired = errors.New("contract expired")
ErrOldSession = errors.New("old session")
ErrOutOfOrder = errors.New("event out of order")
)
type Check struct {
AuthenticatedBidder string
CurrentAuction string
CurrentSession uint64
IsMember bool
NowUnix int64
ExpiresUnix int64
LastSequence uint64
}
func Authorize(c Check, e Envelope) error {
if c.AuthenticatedBidder == "" || e.BidderID != c.AuthenticatedBidder {
return ErrWrongAudience
}
if e.AuctionID != c.CurrentAuction || !c.IsMember {
return ErrNotMember
}
if e.ExpiresAt.Unix() <= c.NowUnix || e.ExpiresAt.Unix() != c.ExpiresUnix {
return ErrExpired
}
if e.SessionVersion != c.CurrentSession {
return ErrOldSession
}
if e.Sequence <= c.LastSequence {
return ErrOutOfOrder
}
return nil
}
Expose each outcome as a low-cardinality metric: wrong audience, membership denial, expired contract, old session, and stale sequence. Decision logs should include event ID, policy version, schema version, and a correlation ID while excluding bid amounts and credentials. A single denied counter cannot distinguish an attack from clock skew or a lagging consumer.
For replay, the bidder supplies its last accepted sequence. The server locates the retained range and reevaluates current membership before releasing it. If storage cannot cover the gap, return a snapshot marker and build a new authorized projection; do not fill an unknown gap with the newest event.
Which tests catch presence errors before rollout?
Turn each invariant into a contract test. Cover an allowed bidder, membership revoked between delivery and replay, a mismatched auction, an expired envelope, duplicate 816, stale 815, a missing range, and an old-session disconnect. Fuzz unknown fields and invalid types. Additive fields may be ignored during a rolling upgrade, but malformed security-critical fields should be rejected rather than given defaults.
Run a failure drill before enabling the contract for every auction. Delay a disconnect, duplicate an event, revoke membership during reconnect, and remove one sequence from replay storage. The expected results are crisp: the newer session survives, the duplicate changes no state, revoked membership blocks replay, and a gap produces a fresh authorized snapshot.
No guessing.
Roll out schema evolution in stages: teach readers the additive field, write it, verify reader-version telemetry, then make it required. Keep the prior reader and policy version during the observation window. Rollback restores the paired writer and authorization policy; reverting transport code alone can leave queued envelopes with semantics an older consumer cannot parse. The catch is operational state: recipient-specific ordering, replay retention, versioned policy records, and renewable leases require more moving parts than a shared broadcast topic. This design is not suitable when notifications are public, presence is decorative, missed messages have no consequence, and every client can refresh cheaply from a canonical snapshot.
Stick with publish-and-refresh in that case. For private auction bidder notifications, stale audience or presence state can expose information or permit the wrong action, so the extra state earns its keep. Transport selection comes after that decision.
Top comments (0)