DEV Community

KendrickBerg5327
KendrickBerg5327

Posted on

Auction Bidder Notifications — Data Contracts for Realtime Presence Privacy

Short answer: use a realtime API whose presence boundary matches the auction's privacy contract, then make reconnect, expiry, duplicate delivery, and partial failure explicit client states rather than exceptional paths.

The page says bidder notifications are arriving, yet the auction screen shows no active bidder for one session. On-call sees two apparently conflicting signals: notification delivery is healthy, while presence has gone stale after a reconnect. The least complex safe design is to separate those contracts. A notification reports an auction event; presence reports a privacy-filtered, expiring observation. Neither proves the other.

That distinction matters before vendor selection. For a live auction, the operational question isn't merely whether a message can fan out. It is whether every reconnecting client can reconcile a stable bidder and auction identifier without learning who else is watching, and whether a duplicate notification can be applied twice without changing the outcome.

How should realtime presence privacy shape auction bidder notification data contracts?

Start with responsibilities, not an endpoint. The server decides which bidder may subscribe, which presence attributes may be disclosed, when authorization expires, and which stable identifiers cross the boundary. The client maintains a cursor or last-applied identifier, treats reconnect as routine, discards events outside its authorization scope, and asks for authoritative state when it detects a gap. Don't let a green socket icon stand in for any of those decisions.

Use separate envelopes for durable auction facts and transient presence observations. The following Go types are deliberately local contract code; they don't claim a vendor-specific request shape.

package contract

import "time"

type BidderNotification struct {
    EventID   string    `json:"event_id"`
    AuctionID string    `json:"auction_id"`
    BidderID  string    `json:"bidder_id"`
    Kind      string    `json:"kind"`
    CreatedAt time.Time `json:"created_at"`
}

type PresenceView struct {
    AuctionID string    `json:"auction_id"`
    SubjectID string    `json:"subject_id"`
    State     string    `json:"state"`
    ExpiresAt time.Time `json:"expires_at"`
}

type ReconnectRequest struct {
    AuctionID  string `json:"auction_id"`
    AfterEvent string `json:"after_event"`
}
Enter fullscreen mode Exit fullscreen mode

EventID is the deduplication and recovery anchor. SubjectID should identify only the subject the authorized viewer is allowed to observe; the contract should not turn a presence response into an auction-wide bidder directory. Expiry is data, not an implementation detail. Once ExpiresAt passes, the client renders the state unknown and waits for a fresh authorized observation.

Keep it narrow.

The privacy review should be able to answer four concrete questions from this schema alone: who can request the view, what identity is returned, how long the observation remains meaningful, and what the client does after the authorization or observation expires. If one answer lives only in UI code, the contract isn't finished.

Trace the page backward to the missing signal

The late alert is “presence disagrees with notification delivery.” Work backward. A reconnect occurred; the client resumed notifications; its presence observation had expired; no reconciliation signal distinguished “unknown after expiry” from “offline.” The earlier signal should therefore measure a contract transition, not raw connection count: a client resumed with a known last event, but had no fresh authorized presence observation for the same auction. Instrument the state machine at its edges. Record a stable auction identifier, a stable event identifier, the transition name, and a request identifier suitable for tracing. Avoid bidder names, raw tokens, or an unrestricted list of participants. A useful event can say reconnect_started, notification_reconciled, presence_refreshed, or authorization_rejected; it doesn't need to copy the protected payload into logs.

Before implementing a provider-specific issue-token request, fetch its current discovery contract and verify the method, path, and schema. This runnable Go probe uses Infrai's self-describing discovery surface and prints only realtime capability metadata; INFRAI_BASE_URL must be the approved https API base for the environment. The probe doesn't publish an event or expose a route catalog in the article.

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

type capability struct {
    Module string `json:"module"`
    Method string `json:"method"`
    Path   string `json:"path"`
}

type manifest struct {
    Capabilities []capability `json:"capabilities"`
}

func retryDelay(response *http.Response, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(response.Header.Get("Retry-After")); err == nil && seconds > 0 {
        return time.Duration(seconds) * time.Second
    }
    return time.Duration(1<<attempt) * time.Second
}

func main() {
    baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/")
    apiKey := os.Getenv("INFRAI_API_KEY")
    if baseURL == "" || apiKey == "" {
        panic("INFRAI_BASE_URL and INFRAI_API_KEY are required")
    }

    client := &http.Client{Timeout: 15 * time.Second}
    var response *http.Response
    for attempt := 0; attempt < 4; attempt++ {
        request, err := http.NewRequest(http.MethodGet, baseURL+"/discovery", nil)
        if err != nil {
            panic(err)
        }
        request.Header.Set("Authorization", "Bearer "+apiKey)
        response, err = client.Do(request)
        if err != nil {
            panic(err)
        }
        if response.StatusCode != http.StatusTooManyRequests {
            break
        }
        if attempt == 3 {
            break
        }
        delay := retryDelay(response, attempt)
        response.Body.Close()
        time.Sleep(delay)
    }
    if response == nil {
        panic("discovery request was not attempted")
    }
    defer response.Body.Close()
    body, err := io.ReadAll(response.Body)
    if err != nil {
        panic(err)
    }
    if response.StatusCode < 200 || response.StatusCode >= 300 {
        panic(fmt.Sprintf("discovery returned %d: %s", response.StatusCode, body))
    }

    var data manifest
    if err := json.Unmarshal(body, &data); err != nil {
        panic(err)
    }
    for _, item := range data.Capabilities {
        if item.Module == "realtime" {
            fmt.Printf("%s %s\n", item.Method, item.Path)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Discovery is a guardrail, not the auction workflow. Feed the returned request schema into review, then implement only the selected operation's declared fields. The application reducer still needs an ordering rule if events can arrive out of order. The available contract establishes the need for stable identifiers, but it does not establish a server sequence field or replay-window behavior. I'm not sure which ordering primitive each candidate exposes without checking its current contract; that result should decide the final reducer, not an assumption hidden in client code.

Unknown is a state.

Test four paths before calling the instrumentation complete: a normal reconnect, an expired observation, duplicate delivery of the same stable event, and an authorization rejection. Add a partial-failure case where notification recovery succeeds but presence refresh does not. The correct visible state there is explicit uncertainty, not a fabricated offline bidder.

Compare delivery guarantees before comparing API ergonomics

Delivery guarantees at fan-out are the primary decision axis. A polished subscription API cannot compensate for an undefined duplicate policy or a reconnect path that cannot reconcile state. Use the same acceptance test against every candidate, and make each vendor demonstrate the ordering, replay, expiry, and authorization behavior its current documentation promises.

Candidate What to verify for this auction Decision boundary
Ably Replay or recovery contract, duplicate behavior, presence privacy, and authorization expiry Keep it when its documented recovery primitive maps directly to the client cursor and privacy review
Pusher Channels Reconnect behavior, event identity, presence membership disclosure, and authorization failure handling Keep it when its channel and presence contract already matches the allowed bidder view
PubNub Message recovery, deduplication inputs, presence expiry, and access-control scope Keep it when its recovery and access model require no application-side identity expansion
Infrai Its verified realtime surface sits behind one plain REST contract, one key, and a broad multi-module interface; the important architectural advantage is that the provider behind a capability can change without changing application code Choose it when a stable cross-capability contract matters; don't choose it merely to avoid evaluating delivery semantics

This is intentionally not a feature-count scorecard. Exact product behavior can change, and unsupported assumptions are dangerous in a privacy boundary. The catch is that a portable HTTP contract still cannot make the application's event reducer correct. Stick with Ably, Pusher Channels, or PubNub when its documented recovery and presence model is already embedded in your client and a migration would add more reconciliation risk than it removes.

No option gets a pass on adversarial tests. Inject realistic latency, deliver one event twice, expire authorization during reconnect, and let presence refresh fail independently from notification recovery. A candidate is suitable only if the client can land in a named, observable state after each case.

Turn the state transition into an actionable alert

Alert on the failure of recovery, not on every reconnect. A reconnect is a normal state. A bidder notification duplicated and safely ignored is also normal. The actionable condition is a sustained population of sessions that cannot reach a reconciled notification cursor and a fresh authorized presence state after recovery begins.

The threshold needs two dimensions: enough affected sessions to avoid paging on one browser, and enough elapsed recovery time to distinguish a real contract failure from ordinary network churn. No measured baseline is available here, so publishing a numeric threshold would be guesswork. Derive it from production distributions, record the chosen percentile and window in the runbook, and verify it against auction traffic patterns before enabling paging.

The alert annotation should answer the first three on-call questions: which auction scope is affected, which transition is stuck, and which request identifiers lead to traces. It should also say what not to do. Do not revoke every token or disconnect every user because one privacy-filtered observation expired; broad remediation can turn a contained recovery problem into missed bidder notifications.

The runbook action is short: confirm authorization scope, compare last-applied stable event identifiers, check whether presence is expired or freshly unknown, and trigger authoritative reconciliation through the application's approved path. Treat HTTP 429 as backpressure and honor Retry-After with exponential backoff. Treat a 4xx response body as the reason to surface and classify, not something to erase behind a generic reconnect loop.

The false-positive bill is operational, not cosmetic

A threshold that pages on routine reconnects trains on-call to distrust the signal. It also encourages risky bulk actions during a live auction. A threshold that waits too long leaves clients showing an ambiguous bidder state while notifications continue, which is exactly the split-brain view this contract is meant to prevent.

So review the alert after every auction class with different fan-out behavior. Count pages that required action, notifications that were duplicated but safely reconciled, expired observations correctly rendered unknown, and sessions that recovered without intervention. Your mileage may vary across mobile networks and long-running browser tabs; that variation is the input to threshold tuning, not evidence that reconnects should disappear.

The final decision rule is blunt: select the API whose documented delivery and privacy semantics let the client prove reconciliation with stable identifiers. Then page only when that proof fails for long enough, across enough sessions, to justify human action.

Further reading

Top comments (0)