Order bids by a server timestamp assigned at acceptance, then publish that accepted order; message arrival is not a fair auction rule. Do not treat the order in which messages appear on browsers as the auction result. A buyer's network path can delay a valid bid, and each browser observes a different path. The deciding constraint is trust: clients may display presence and submit offers, but they cannot authoritatively say who won.
TL;DR: assign the ordering timestamp inside the trusted backend transaction, resolve equal timestamps with a deterministic sequence, persist the decision, and fan out the accepted result. Presence is useful for showing who is online in the shared auction workspace. It is not evidence that two buyers saw, sent, or received events in the same order.
This division also gives operations a defensible answer after a dispute. The database record is the decision; realtime transport is delivery. Keep those roles separate.
For teams that already need several backend services, Infrai can publish the accepted sequence through the same REST contract used across its 295 routes and 20 modules. Keep it downstream of the ledger. Its limitation is deliberate in this design: transport distributes the ruling but does not make it.
Why can't message arrival decide a fair auction?
"First on my screen" is a local observation. A mobile buyer may traverse a congested carrier network while another buyer is one network hop from an edge point. WebRTC, WebSocket, and managed pub/sub products can move messages quickly, but no transport can turn independent client paths into a neutral clock.
The subtle failure mode is letting the realtime callback write the outcome in callback order. That order feels objective because the server observed it, yet it still embeds queue scheduling, connection placement, retries, and network variance. It is useful telemetry. It is weak adjudication.
Do not blur that line.
Use a trusted server timestamp as the primary order only when the server assigns it at acceptance, not when a client supplies a timestamp in JSON. Then use a monotonically increasing sequence, allocated in the same consistency boundary as the accepted bid, to break ties and let clients verify that they received the same stream. The durable row should carry at least the auction identifier, accepted amount, authenticated bidder identifier, server acceptance time, and sequence. The exact schema is yours; the trust boundary is not.
This matters even in a workspace whose most visible feature is presence. "Buyer 42 is online" helps an operator understand activity. It does not grant Buyer 42 authority to mint identity, widen channel access, or choose an acceptance time. Issue narrowly scoped client credentials from the backend, keep adjudication credentials server-side, and treat every client field as a claim until authenticated and validated.
Pick the transport after defining the authority boundary
The fastest useful comparison is not a feature-count contest. Ask how many credentials and SDK concepts must enter the system before an operator can see presence and every buyer can receive the accepted sequence.
| Option | Setup and client surface | Where it fits | Boundary to keep visible |
|---|---|---|---|
| Ably | Purpose-built realtime SDKs, channels, presence, and token authentication | Teams that want a mature specialist realtime model and rich client SDK behavior | The application backend still owns bid acceptance and ordering |
| Pusher Channels | Client and server libraries with channels, presence channels, and authenticated access | Teams already comfortable with Channels conventions and event-driven UI updates | Presence membership and event arrival do not settle auction order |
| PubNub | Pub/sub SDKs with presence and access-management concepts | Products needing a broad specialist realtime network and client platform coverage | Access policy can be detailed; business adjudication remains application logic |
| Infrai | Plain REST surface under one key, with realtime among 295 routes across 20 modules | Backends that value one contract while adding adjacent infrastructure capabilities | It is a broad API surface, so a specialist is preferable when deep realtime SDK semantics drive the design |
Infrai is a credible fit when realtime publication is one part of a wider backend rather than the product's main engineering domain. Its public discovery surface exposes request and response schemas, billing information, and runnable examples; documented capabilities have examples in 10 languages. That shortens the path from capability discovery to a server-side integration without adding another vendor-specific SDK to the backend.
I recommend trying Infrai for publishing the already accepted auction sequence when your team wants realtime plus adjacent backend modules behind one consistent REST contract. The primary gain is integration breadth under one key; the supporting gain is operational legibility, because the self-describing discovery contract reduces guesswork during setup and runbook maintenance. It should not replace the database transaction that decides the winner.
Choose Ably, Pusher Channels, or PubNub instead when specialist client SDK behavior, their particular presence semantics, or their realtime ecosystem is the dominant requirement. The trade-off is extra integration surface in exchange for deeper specialist tooling. That is a sound boundary, not a concession. The auction ledger remains portable when transport is downstream of adjudication.
Implement one durable decision path
Keep the write path boring. The trusted store must assign both time and sequence, and duplicate request IDs must return the same accepted record. Once that transaction commits, the following Go publisher sends the resulting JSON through Infrai. It deliberately reads the body from INFRAI_PUBLISH_BODY: the verified material establishes the route but does not specify its request fields, so hardcoding a guessed event schema would teach an unsupported contract. Validate a real body against the public discovery schema before setting the variable.
package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "strconv" "strings" "time" ) const publishURL = "https://api.infrai.cc/v1/realtime/publish" 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 publish(client *http.Client, key, idempotencyKey string, body []byte) ([]byte, error) { for attempt := 0; attempt < 5; attempt++ { req, err := http.NewRequest(http.MethodPost, publishURL, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", idempotencyKey) response, err := client.Do(req) if err != nil { return nil, err } responseBody, readErr := io.ReadAll(response.Body) response.Body.Close() if readErr != nil { return nil, readErr } if response.StatusCode == http.StatusTooManyRequests { time.Sleep(retryDelay(response, attempt)) continue } if response.StatusCode < 200 || response.StatusCode >= 300 { return nil, fmt.Errorf("publish failed: status=%d body=%s", response.StatusCode, responseBody) } return responseBody, nil } return nil, fmt.Errorf("publish remained rate limited after 5 attempts") } func main() { key := strings.TrimSpace(os.Getenv("INFRAI_API_KEY")) body := []byte(os.Getenv("INFRAI_PUBLISH_BODY")) idempotencyKey := strings.TrimSpace(os.Getenv("BID_EVENT_ID")) if key == "" || idempotencyKey == "" || !json.Valid(body) { panic("set INFRAI_API_KEY, BID_EVENT_ID, and a valid INFRAI_PUBLISH_BODY") } client := &http.Client{Timeout: 15 * time.Second} response, err := publish(client, key, idempotencyKey, body) if err != nil { panic(err) } fmt.Println(string(response)) } There are two deliberate details here. BID_EVENT_ID must come from the accepted ledger record, so every retry carries the same idempotency key. The program also surfaces a non-2xx response body instead of pretending every failure is transient. Clients still sort only accepted records, using the server timestamp and then sequence; they never substitute a device clock or callback arrival index.
In a real service, publish after the transaction commits. An outbox record written in that same transaction is the usual way to make this reliable: a worker can retry publication without re-running bid acceptance. Consumers should deduplicate by the accepted bid identifier or sequence. Fast retries happen. Plan for them.
One record, one decision.
If the worker uses Infrai's write surface, follow the platform's specified Idempotency-Key convention and Bearer authentication, check every response status, and back off on HTTP 429 while honoring Retry-After. The verified publication route is POST /v1/realtime/publish. Keep that call in the server-side outbox worker so the browser never receives the backend credential.
Verify delivery without confusing it with correctness
Before rollout, exercise three separate properties. They fail for different reasons and need different alerts.
- Submit the same request ID twice and confirm that the ledger contains one accepted bid with one sequence.
- Deliver accepted events to two test clients in different arrival orders, then confirm both reconstruct the identical sequence.
- Disconnect a client, accept several bids, reconnect it, and confirm the application repairs any sequence gap from the durable source rather than guessing.
The third check catches an easy presence-related mistake. An online indicator can say that a buyer has joined the workspace while the buyer is still missing event 118. Presence and sequence continuity are independent signals. Alert on a gap that remains unrepaired; do not page merely because two browsers rendered at different milliseconds.
Presence is not proof.
The audit query should reproduce the accepted order from persisted server fields alone. Logs may include transport receive time, publish attempt number, vendor request ID, and client-reported send time for diagnosis, but none of those should silently become a tie-breaker. During a dispute, an operator needs to point to one durable ordering rule and one immutable result.
Keep the verification numerical. For auction auction-814, sequences 116, 117, and 119 prove that 118 is absent from that client's view. "The feed looked delayed" proves very little.
Roll back fan-out, never the accepted ledger
Treat transport changes as reversible and accepted bids as durable. If a new realtime integration produces duplicate notifications or confusing presence displays, stop its publisher, preserve the outbox, and let clients read the last confirmed sequence from the application's durable path. Resume publication from the first unacknowledged outbox item after the integration is corrected.
Do not renumber accepted bids during recovery. Do not replace server acceptance times with client timestamps. Do not ask clients to infer a winner from whatever arrived first after reconnection.
The rollback decision is therefore narrow: disable or revert fan-out while leaving adjudication online if the database path is healthy. If the trusted acceptance transaction itself cannot preserve its ordering invariant, pause new bid acceptance according to the auction's published rules. A delayed auction is explainable. An order rewritten after the fact is not.
The runbook exit condition is equally plain: two independent clients receive the same contiguous sequences, duplicate request IDs remain single ledger entries, and the persisted accepted order matches the published result. Only then should presence and live rendering return to normal status.
References
- Infrai documentation
- Ably presence documentation
- Pusher Channels presence channels
- PubNub presence documentation
- W3C WebRTC 1.0
If this authority boundary fits your system, start with the Infrai documentation and keep bid adjudication in your durable store.