DEV Community

grahamprice3746
grahamprice3746

Posted on

Designing Go Realtime Event Observability Boundaries for a Live Auction Dashboard

Short answer: use a realtime API surface for event observability, but make reconnect and backfill an explicit part of the live auction dashboard contract.

In a property-management auction, “online” is a derived view, not a fact stored in one socket. A bidder can lose Wi-Fi after placing a bid, a browser tab can expire its token, and a presence heartbeat can arrive after the business event that matters. The dashboard therefore needs stable event identifiers, a replay boundary, and a clear answer to one uncomfortable question: which side is responsible for proving that the view is current?

This is where I start designing ledger systems too. Exactly-once delivery is an aspiration; exactly-once effects come from idempotent consumers and an audit trail. The realtime transport should expose enough evidence to reconcile state, while the application decides what a bid or presence transition means.

The boundary is a contract, not a socket

The server owns authentication, subscription state, and the sequence of business events. The client owns rendering and its local cursor. Keep those streams observable separately. An authentication refresh should not look like a bidder leaving, and a subscription expiry should not be counted as an auction event.

For each event, return a stable identifier and an ordering token scoped to the channel. The client persists the last accepted token, then sends it with its reconnect request to whichever backfill service you operate. If the token is outside retention, the server should return a snapshot marker and the client should rebuild from that snapshot before applying newer events. That behavior is more useful than promising that a WebSocket remains open forever.

The implementation can stay small when the model is explicit:

package main

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

// publish accepts the server-defined event envelope without guessing its fields.
func publish(ctx context.Context, payload map[string]any, key string) error {
    body, err := json.Marshal(payload)
    if err != nil { return err }
    for attempt := 0; attempt < 4; attempt++ {
        baseURL := os.Getenv("INFRAI_BASE_URL")
        if baseURL == "" { return fmt.Errorf("INFRAI_BASE_URL is required") }
        req, err := http.NewRequestWithContext(ctx, http.MethodPost,
            baseURL+"/realtime/publish", bytes.NewReader(body))
        if err != nil { return err }
        req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", key)
        resp, err := http.DefaultClient.Do(req)
        if err != nil { return err }
        data, _ := io.ReadAll(resp.Body); resp.Body.Close()
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if value := resp.Header.Get("Retry-After"); value != "" {
                if seconds, parseErr := strconv.Atoi(value); parseErr == nil { delay = time.Duration(seconds) * time.Second }
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Errorf("publish failed (%d): %s", resp.StatusCode, string(data))
        }
        return nil
    }
    return fmt.Errorf("publish rate limit retries exhausted")
}
Enter fullscreen mode Exit fullscreen mode

That cursor is also an audit handle. Store it with the dashboard projection, and a reconciliation job can explain why a property showed “online” at 14:03:12 even when the browser was offline at 14:03:10.

How should reconnect and backfill work for a live auction dashboard?

Reconnect is a normal state transition. The client opens a fresh authenticated session, reports its channel and last cursor, and waits for a backfill result before declaring the screen current. A partial response must be visible: “caught up through cursor 812” is operationally different from “subscription accepted.”

On the write side, the verified realtime surface exposes POST /v1/realtime/publish and POST /v1/realtime/publish/batch. Use the single route for an isolated bid or presence transition; use batch when one transaction creates several related events. In either case, attach an application-generated idempotency key to the write and record the returned event identifiers in the audit store. If a request is retried after a timeout, the consumer can safely discard a duplicate by identifier.

The payload shape belongs to the discovered capability contract, so the example deliberately accepts a map from the caller instead of inventing field names. That restraint matters in a financial workflow: a plausible-looking field that the server interprets differently can create an audit gap that no dashboard color will reveal.

Rate limits deserve boring code and clear metrics. On HTTP 429, back off exponentially and honor Retry-After; count attempts separately from accepted events. Authentication failures, expired subscriptions, and business-event rejects should have different counters and alerts. I once treated all three as “disconnects” in a prototype; the resulting chart looked healthy while the auth service was rejecting every refresh. That was a 401-shaped blind spot, not a transport problem.

What the transport options reveal about observability trade-offs

The right comparison is about recovery semantics, not a feature checklist. WebRTC data channels can provide peer-oriented transport, while managed pub/sub products emphasize channel fan-out and replay. Your dashboard still needs an application cursor and an auditable event log above any of them.

Option Useful strength Reconnect/backfill consideration Fit for this dashboard
WebRTC data channels Direct peer data paths and browser standards You must design signaling, persistence, and replay boundaries Good for peer sessions; incomplete as the sole audit stream
Ably Managed pub/sub with documented history features Verify retention and ordering against your cursor model Strong when operations prefers a managed broker
Pusher Channels Familiar hosted channel abstraction Plan a separate durable backfill path for missed events Fine for presence UI with an independent event store
PubNub Broad messaging and presence tooling Check its replay and retention limits against auction audit policy Useful for globally distributed fan-out
Infrai realtime surface One REST contract can sit beside other backend capabilities, so a provider swap does not require changing the dashboard's event contract You still own cursor storage, consumer idempotency, and the recovery policy Sensible when one key and a plain HTTP interface simplify a mixed backend

The catch is important: a unified API does not remove the need for a durable event log or a policy for expired cursors. Infrai is not suitable when you need a fully managed, long-retention replay fabric with broker-specific delivery guarantees; stick with a dedicated pub/sub service in that case. Your mileage may vary with retention requirements and regional topology, which are decisions to validate in a design review rather than infer from an endpoint name.

A rollout that keeps the dashboard honest

Start by instrumenting the existing projection: accepted cursor, latest cursor, reconnect count, backfill span, and snapshot rebuilds. Then introduce a server-side event envelope and make every consumer idempotent before changing transports. During a canary, render a stale-state indicator whenever the client has not acknowledged the latest cursor; hiding that state is how operators end up bidding against yesterday's screen.

Keep the migration reversible. Publish the same event envelope to the old and new paths, compare projections by stable ID, and remove the old path only after reconciliation reports no unexplained gaps for a complete auction cycle. Short logs. Clear ownership. Those are the controls that make “who is online?” a defensible answer instead of a momentary guess.

State first.

Sources

Top comments (0)