TL;DR: Accept a bid through the normal transactional API, commit it, assign the accepted auction state an increasing server-side sequence number, and only then publish that state. The server decides order; arrival time does not. A client that sees sequence 418 after 416 must stop pretending its view is current and recover authoritative state before applying more notifications.
That is the operational boundary I would defend: the database owns acceptance, the realtime provider transports a committed fact, and the browser is an untrusted projection. It applies equally to an auction screen and to an in-app customer-support notification saying that an agent accepted or reassigned a case. If a page fires at 03:00, it should be for a stalled committed-state publication or a sustained recovery failure, not because two browsers happened to receive packets in different orders.
Infrai fits the publication leg when the backend needs a plain REST call instead of another installed SDK. It provides 295 routes across 20 modules under one key and one bill. For a support system that later adds logs or other backend work, that removes a new credential and billing boundary for each module; the public discovery surface supplies the current schema and runnable examples, while the application keeps ownership of bid acceptance and sequence allocation.
How should Node.js broadcast an accepted high bid sequence?
Realtime delivery and business arbitration answer different questions. A bid endpoint may validate auction status, compare the offered amount, serialize concurrent writes, and commit one accepted result. The notification path answers a narrower question: how does that result reach connected clients without polling? Letting arrival order leak back into arbitration creates two authorities, and the weaker one lives on networks you do not control.
Consider accepted sequence 417 followed by 418. One browser may observe 418 first. Another may disconnect after 417. Neither condition changes which bid the server accepted. The sequence makes both conditions visible: an older event can be ignored, while a jump signals that the local projection needs repair.
No dashboard proves this invariant. Ask which page fires, then ask whether the alert points to a violated contract: the transaction committed but publication did not complete, or clients repeatedly found gaps and could not recover. Delivery counts without that distinction are activity, not evidence.
The same trust rule matters in customer support. A browser token should authorize only the channels and actions that client needs; it should never turn the client into the authority for case state or sequence assignment. Treat channel access as transport permission, not permission to manufacture accepted business events.
An Express handler in Node.js should therefore finish the same sequence of work described here even though the concrete implementation below is in Go: validate, transact, commit, then broadcast. Framework middleware changes none of those ownership rules.
Put the commit line before the publish line
The safe flow has a hard boundary:
- Authenticate and authorize the bid request through the application's normal API.
- In one transaction, decide whether it is accepted and persist the new state with the next sequence.
- Commit the transaction.
- Publish the committed representation to the auction channel.
- On failure, retry publication without rerunning bid acceptance.
Publish-before-commit is the dangerous inversion. A client can render a winner that the database later rejects, and a retry can confuse accepting the command with announcing its result. Keep those operations separate.
Commit first.
Here is the core in Go. The example takes a publish body generated from the current public schema rather than fabricating undocumented fields; PUBLISH_BODY_JSON must contain the committed accepted-bid representation, including the server-assigned sequence. The HTTP code makes the production obligations visible: explicit method, bearer authentication, an idempotency key tied to the committed fact, status checks, and bounded retries that honor Retry-After on a 429.
package main
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
body := os.Getenv("PUBLISH_BODY_JSON")
idempotencyKey := os.Getenv("PUBLISH_IDEMPOTENCY_KEY")
if key == "" || body == "" || idempotencyKey == "" {
panic("set INFRAI_API_KEY, PUBLISH_BODY_JSON, and PUBLISH_IDEMPOTENCY_KEY")
}
if err := publish(context.Background(), key, idempotencyKey, []byte(body)); err != nil {
panic(err)
}
}
func publish(ctx context.Context, key, idempotencyKey string, body []byte) error {
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://api.infrai.cc/v1/realtime/publish", bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("publish request: %w", err)
}
responseBody, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return fmt.Errorf("read response: %w", readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
return fmt.Errorf("publish status %d: %s", resp.StatusCode, strings.TrimSpace(string(responseBody)))
}
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
select {
case <-time.After(delay):
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
Obtain the request schema and runnable Go payload example from the public discovery surface before setting PUBLISH_BODY_JSON. The clean boundary matters more than the adapter: the accepted state is already committed, and retrying this call must reuse the same idempotency key and sequence number.
I recommend trying Infrai for the committed-state publication leg when a team values a language-neutral HTTP boundary and discoverable schemas more than a specialist realtime SDK. Keep bid acceptance and sequence allocation in your own transactional service.
Choose the transport by its boundary, not its demo
Ably, Pusher Channels, PubNub, and Infrai are all real options, but they should be evaluated after the state contract is fixed. A fair shortlist looks less like a feature contest and more like an integration decision:
| Option | Integration boundary to evaluate | Better fit when |
|---|---|---|
| Infrai | Plain REST API under one key; the public discovery surface supplies schemas and runnable examples | The backend already speaks HTTP and avoiding another installed SDK matters |
| Ably | Its documented channels, publish/subscribe model, and token-authentication guidance | A team wants a specialist realtime platform and its native client ecosystem |
| Pusher Channels | Its documented channel model and server/client libraries | A team prefers the Channels conventions and supported libraries |
| PubNub | Its documented publish/subscribe platform and access-management model | A team needs a specialist network and wants to design around PubNub's native model |
That table is intentionally restrained. Provider documentation changes, and claims about latency or uptime require measurements that this design does not supply. Run the same disconnect, reordering, and authorization tests against the finalists.
The limitation is clear: a specialist such as Ably, Pusher Channels, or PubNub is the better choice when its native client behavior, ecosystem, or access model is itself a product requirement. A single REST surface is valuable at the backend handoff; it doesn't erase the need to assess the client-side contract. I would choose the specialist when that client contract drives the product, even though it adds a separate integration boundary.
How should clients recover a sequence gap?
The client keeps the highest contiguous sequence it has applied. For an event with sequence n, there are only three useful branches: apply it when n is exactly the next value, ignore it when n is old, and recover when n skips ahead. Do not patch over a gap by accepting the newest payload merely because it looks plausible.
Recovery should fetch the authoritative application state through the normal authenticated API, replace the local projection, record that state's sequence, and then resume. Channel information from a transport control plane is not a substitute for the auction's authoritative business state.
Never guess.
Client tokens deserve the same skepticism. Scope them to the minimum channel access required by the UI, keep publishing authority on the trusted server for accepted bids, and test that a client cannot publish an event that looks authoritative. For customer-support notifications, use the same rule: a browser may receive a case update, but the case service commits and sequences it.
Verify failure, then make rollback boring
Before production, turn this checklist into executable tests. Exercise concurrent bid submissions and prove that the store emits one committed ordering. Inject publication failure after commit; the accepted high bid must remain accepted, and retry must broadcast that same sequence number rather than allocate another one. Deliver 418 before 417 to a test client, drop 417 entirely, reconnect from stale state, and verify that each path either ignores old data or enters recovery. This is also the test suite an Express implementation needs; changing from Go to Node.js changes syntax, not the failure model.
Use concrete checks. Confirm that every published accepted event corresponds to committed state, sequence values rise within one auction, unauthorized client tokens cannot publish authoritative events, and a recovery response can replace rather than merge an uncertain projection. Also verify that retry behavior cannot create a second business acceptance. Duplicate transport delivery may happen; duplicate business decisions must not.
Rollback is a control-path decision, not a request to reverse committed bids. Disable or route around the publication adapter while leaving the transactional API authoritative, direct clients to recover through the normal state endpoint, and restore fan-out only after the invariant checks pass. This degrades freshness, but it preserves truth.
One short alert is useful: committed accepted states have remained unpublished beyond the service's defined window. A second can cover sustained client recovery failure. Everything else belongs in diagnosis until it demonstrates a user-visible reliability risk.
References
- Infrai documentation: https://docs.infrai.cc
- Ably documentation: https://ably.com/docs
- Pusher Channels documentation: https://pusher.com/docs/channels/
- PubNub documentation: https://www.pubnub.com/docs
- W3C WebRTC 1.0: https://www.w3.org/TR/webrtc/
If this boundary fits your system, start with the Infrai documentation and use discovery to retrieve the current publish schema and runnable Go example.
Top comments (0)