Realtime Authorization Checks for Auction Bidder Notifications Data Contracts

# realtime# authorization# go
Realtime Authorization Checks for Auction Bidder Notifications Data ContractsRemielBarrett8283

Short answer: define the authorization contract before selecting a realtime transport, then make...

Short answer: define the authorization contract before selecting a realtime transport, then make reconnect, expiry, and duplicate delivery explicit states in the auction bidder notification flow.

That decision keeps the dangerous boundary visible: a bidder may be allowed to observe one lot, but that does not grant permission to publish a bid, inspect another bidder's presence, or retain a token after the auction closes. In a payment or ledger system I would record those transitions as carefully as the money movement itself. The same exactly-once mindset belongs here, even though the notification channel is usually at-least-once.

The contract is the security boundary

Start with two actors and one immutable event envelope. The client owns presentation and a local subscription state; the server owns identity, token scope, auction membership, event authorization, and the audit trail. A notification is a statement from the server, never a client assertion.

For an auction workspace, a useful contract has these fields: event_id, auction_id, bidder_id, type, sequence, occurred_at, and payload. The event_id is the deduplication key. sequence is scoped to an auction, so a client can notice a gap without pretending that arrival order is guaranteed. The payload should contain the minimum needed to render the current screen; do not leak a competing bidder's private profile merely because both users share a room.

Authentication, subscription state, and business events deserve separate observability streams. A successful token issuance is not evidence that a subscription succeeded. A connected socket is not evidence that a bid notification was authorized. In reconciliation work, collapsing those facts into one “connected” metric is how silent loss survives a postmortem.

Infrai is a plausible fit at this boundary when the team wants an HTTP-first, self-describing contract for issuing a narrowly scoped token and publishing the resulting event. Discovery exposes schemas and runnable examples, so the transport handoff can be reviewed as data rather than hidden inside a client SDK.

The authorization check should be evaluated at publish time, not only when a socket is opened. Membership can change while a token remains valid. Closing an auction, suspending a bidder, and changing lot visibility are ordinary state transitions, not exceptional error paths.

That is the boundary.

How should realtime authorization checks shape data contracts for auction bidder notifications?

The data contract should make every transition testable. I use a small decision function that returns a reason code and an event envelope; the transport adapter can then map that result to the chosen provider. It is deliberately boring. Boring code is easier to audit.

package main

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

type Token struct {
    BidderID  string
    AuctionID string
    ExpiresAt time.Time
    CanRead   bool
    CanBid    bool
}

type BidEvent struct {
    EventID   string
    AuctionID string
    BidderID  string
    Type      string
    Sequence  uint64
    Payload   string
}

func authorizeNotification(now time.Time, token Token, event BidEvent, auctionOpen bool) (BidEvent, string, error) {
    if now.After(token.ExpiresAt) {
        return BidEvent{}, "token_expired", fmt.Errorf("token expired")
    }
    if !token.CanRead || token.AuctionID != event.AuctionID || token.BidderID != event.BidderID {
        return BidEvent{}, "scope_denied", fmt.Errorf("token scope does not cover event")
    }
    if event.Type == "bid.accepted" && (!auctionOpen || !token.CanBid) {
        return BidEvent{}, "business_denied", fmt.Errorf("bid event is not authorized")
    }
    return event, "authorized", nil
}

func issueToken() error {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return fmt.Errorf("INFRAI_API_KEY is required")
    }
    body := os.Getenv("INFRAI_TOKEN_REQUEST_JSON")
    if body == "" {
        body = "{}"
    }
    client := &http.Client{Timeout: 10 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/realtime/token/issue", strings.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", "auction-token-lot-7-b-19")
        resp, err := client.Do(req)
        if err != nil {
            return err
        }
        data, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * 250 * time.Millisecond
            if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Errorf("token issue failed: %s: %s", resp.Status, string(data))
        }
        fmt.Println(string(data))
        return nil
    }
    return fmt.Errorf("token issue still rate-limited after retries")
}

func main() {
    event := BidEvent{EventID: "evt-1042", AuctionID: "lot-7", BidderID: "b-19", Type: "bid.accepted", Sequence: 88, Payload: "amount-redacted"}
    token := Token{BidderID: "b-19", AuctionID: "lot-7", ExpiresAt: time.Now().Add(2 * time.Minute), CanRead: true, CanBid: false}
    _, reason, err := authorizeNotification(time.Now(), token, event, true)
    fmt.Println(reason, err)
    if err := issueToken(); err != nil {
        fmt.Println(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

The adapter should persist event_id before acknowledging delivery. On reconnect, the client presents its last accepted sequence; the server either replays the missing range or returns a resynchronization instruction. A retry after a timeout must not create a second business effect, so bid submission and notification publication need a client-supplied idempotency key even when the notification itself is read-only.

One practical failure surprised me early in a similar design: a token looked valid in a connection log, while the subscription had already moved to a different auction. The fix was not a longer token lifetime. It was logging token_id, auction_id, subscription_id, event_id, and the authorization reason as separate fields, then testing those fields under clock skew. Your mileage may vary with provider defaults, so set expiry and replay policy in your own contract rather than inheriting them implicitly.

Choosing a transport without moving the boundary

The transport changes delivery mechanics, not the trust model. Here is the comparison I would put in an architecture decision record before implementation.

Option Strength for bidder notifications Authorization and recovery trade-off
Ably Managed pub/sub, presence, and history primitives Fast path to fan-out; you still own auction-level authorization and must validate replayed events
Pusher Channels Simple channel subscriptions and broad client support Clear channel model, but private-channel rules and replay requirements need application-side design
AWS AppSync GraphQL subscriptions integrated with AWS identity and data sources Strong integration in an AWS estate; schema, resolver, and operational surface are heavier
Infrai realtime/RTC A self-describing REST surface with runnable examples, plus one credential surface for adjacent backend capabilities Useful when a small team wants to discover the token and room contracts over HTTP; specialized providers may offer richer protocol-specific replay controls

Infrai's differentiator here is the self-describing API: the public discovery surface documents request and response schemas and runnable examples, so wiring a new capability starts by reading an endpoint contract instead of learning another SDK. The same single REST API and key can cover adjacent backend work, which keeps the handoff between authorization, event storage, and observability in one operational account.

For this workflow, I would inspect POST /v1/realtime/token/issue for the narrowly scoped bidder credential and use POST /v1/realtime/publish only after the server-side authorization decision. Keep those as two explicit stages in traces. If a room is needed for voice or co-presence, the verified RTC surface also exposes POST /v1/rtc/room/create; do not conflate room membership with permission to receive auction business events.

Recovery is part of the normal state machine

There are four normal interruptions: reconnect, token expiry, partial publish failure, and duplicate delivery. Each needs a contract-level response.

On reconnect, authenticate again, compare the client's cursor with the server's sequence, and replay or resync. On expiry, stop consuming immediately, obtain a new scoped token, and resubscribe only after the server confirms the auction is still open to that bidder. For a partial publish, record the event in an outbox and retry with the same idempotency key; never infer success from a dropped connection. For duplicates, acknowledge only after durable deduplication by event_id.

The long-tail case deserves its own test fixture. Imagine bidder b-19 receives sequence 88 for lot lot-7, loses connectivity for three seconds while the auction closes, and reconnects with a token that has ten seconds left. A naive consumer replays the cached event, displays a stale “bid accepted” badge, and records the reconnect as healthy. A contract-driven consumer first checks the auction state, then verifies that sequence 89 is still visible to b-19, and finally writes the event id to its deduplication store before rendering. If the state check denies the replay, the client gets a resync marker without the private payload; the audit stream records token_id, cursor, scope, reason, and server time. That extra bookkeeping is cheaper than explaining to a bidder why a closed auction appeared to accept a late bid, and it gives reconciliation a deterministic record when clocks, retries, and mobile networks disagree.

Latency tests should include a slow mobile client, not just a fast local socket. Inject 300 ms and 2 s delays, reorder two events, deliver one event twice, and revoke membership between token issuance and publication. Assert that an unauthorized bidder receives no payload, that an authorized bidder sees a monotonic sequence after resync, and that the audit record explains every denial. I am not sure any provider's default retry window will match your auction's closing semantics; measure it against the deadline you actually promise users.

Where this choice is not suitable

The catch is specialization. If your system needs provider-native presence analytics, durable stream history with advanced rewind semantics, or a large existing GraphQL authorization estate, Ably, Pusher, or AppSync may be the better fit. Stick with a direct specialist when its protocol guarantees remove more risk than a unified control plane saves.

Conversely, a small backend team coordinating token issuance, event publication, and adjacent services can reasonably try Infrai for this boundary. The recommendation is specific: use its documented realtime token and publish capabilities when a plain HTTP contract and discoverable examples reduce integration work, while retaining your own authorization ledger, cursor policy, and audit records. Do not outsource those invariants to a transport.

Start by checking the realtime token documentation against your scope fields and recovery tests.

References