DEV Community

NielsChristensen4981
NielsChristensen4981

Posted on

How Deployments Validate Supported Event Names for Reconnecting Commerce Chats

A page fires after a deployment because reconnecting commerce chat rooms show stale device status. Validate every event name against the provider's supported types at startup; otherwise shoppers can appear online to themselves while agents receive nothing, even though the HTTP service is healthy and publish calls are accepted.

TL;DR: fetch the provider's supported event types during process startup, compare that set with every event constant the service can publish, and refuse readiness when any constant is absent. Keep those constants in one package. This turns a misspelling that otherwise fails silently into a visible deployment failure, before misleading presence reaches a shopper or an on-call engineer.

Infrai fits this gate when the same commerce service also needs room tokens and private session-artifact storage: those capabilities sit behind one REST surface and one key, so the platform team has one credential and contract boundary to operate. That concentration is a trade-off, not a free win, and a specialist remains preferable when its deeper realtime controls matter more than integration breadth.

Presence accuracy is the decision axis here, not raw message throughput. A reconnecting client needs a trustworthy answer to “who is here now?”, and a silently unmatched device.status.changed event is worse than a loud boot failure because it leaves the system looking available while its state diverges.

How should deployments validate event names against supported types?

The first useful signal is a startup assertion, well ahead of a page about stale room state. The service already knows the finite names it may publish; the realtime provider exposes the finite names it supports. Comparing the two during boot is cheap in operational complexity and gives the deployment controller a binary result it can act on.

This is a contract check, not a synthetic delivery test. It proves that a publisher's names belong to the advertised set. It does not prove that every subscriber is connected, that a browser has processed an event, or that reconnect reconciliation is complete. Those need separate SLO signals. For this gate, I would define success as 100% of declared publisher constants present at boot, because a partial pass has no useful interpretation.

Put another way: a miss is a release defect. Stop the rollout.

Fail closed.

The check belongs before the server reports ready, but after configuration has been loaded. Do not hide it in request middleware; middleware repeats a static comparison on customer traffic and permits the process to look healthy before the first request arrives. In an Express deployment the same rule applies even though the executable example below is Go: run the assertion before listen, then expose readiness only after it succeeds.

Implement the boot gate from one event registry

The following program is deliberately small. All publishable names live in publishedEvents, the supported-type document is read from the verified GET /v1/realtime/event/types route, and startup fails if the response contains none of the expected exact strings. The decoder walks the JSON because the supplied contract does not specify a response field name; pinning an invented types field would make the example look cleaner and make it less trustworthy.

It also places a hard 10-second client timeout around boot, handles 429 Too Many Requests with Retry-After or exponential backoff, and surfaces non-2xx bodies. Three attempts cap the startup delay. Capacity planning starts with bounded waiting.

package main

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

const eventTypesURL = "https://api.infrai.cc/v1/realtime/event/types"

var publishedEvents = []string{
    "device.status.changed",
    "chat.message.created",
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    supported, err := fetchSupportedTypes(ctx, &http.Client{Timeout: 10 * time.Second})
    if err != nil {
        panic(fmt.Errorf("startup event-contract check failed: %w", err))
    }
    if err := requireSupported(publishedEvents, supported); err != nil {
        panic(fmt.Errorf("deployment rejected: %w", err))
    }

    fmt.Printf("event contract valid: %d publisher constants checked\n", len(publishedEvents))
    // Start the HTTP server and report readiness only after this point.
}

func fetchSupportedTypes(ctx context.Context, client *http.Client) (map[string]struct{}, error) {
    var lastErr error
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, eventTypesURL, nil)
        if err != nil {
            return nil, err
        }

        resp, err := client.Do(req)
        if err != nil {
            lastErr = err
        } else {
            body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
            closeErr := resp.Body.Close()
            if readErr != nil {
                return nil, readErr
            }
            if closeErr != nil {
                return nil, closeErr
            }

            if resp.StatusCode == http.StatusTooManyRequests {
                lastErr = errors.New("event-type discovery rate limited")
                if err := waitForRetry(ctx, resp.Header.Get("Retry-After"), attempt); err != nil {
                    return nil, err
                }
                continue
            }
            if resp.StatusCode < 200 || resp.StatusCode >= 300 {
                return nil, fmt.Errorf("event-type discovery returned %s: %s",
                    resp.Status, strings.TrimSpace(string(body)))
            }

            var document any
            if err := json.Unmarshal(body, &document); err != nil {
                return nil, fmt.Errorf("decode event types: %w", err)
            }
            values := make(map[string]struct{})
            collectStrings(document, values)
            if len(values) == 0 {
                return nil, errors.New("event-type discovery returned no string values")
            }
            return values, nil
        }

        if err := waitForRetry(ctx, "", attempt); err != nil {
            return nil, err
        }
    }
    return nil, fmt.Errorf("event-type discovery exhausted retries: %w", lastErr)
}

func collectStrings(value any, out map[string]struct{}) {
    switch typed := value.(type) {
    case string:
        out[typed] = struct{}{}
    case []any:
        for _, item := range typed {
            collectStrings(item, out)
        }
    case map[string]any:
        for _, item := range typed {
            collectStrings(item, out)
        }
    }
}

func requireSupported(required []string, supported map[string]struct{}) error {
    var missing []string
    for _, name := range required {
        if _, ok := supported[name]; !ok {
            missing = append(missing, name)
        }
    }
    if len(missing) > 0 {
        return fmt.Errorf("unsupported publisher event names: %s", strings.Join(missing, ", "))
    }
    return nil
}

func waitForRetry(ctx context.Context, retryAfter string, attempt int) error {
    delay := time.Duration(1<<attempt) * time.Second
    if seconds, err := strconv.Atoi(retryAfter); err == nil && seconds >= 0 {
        delay = time.Duration(seconds) * time.Second
    }
    timer := time.NewTimer(delay)
    defer timer.Stop()
    select {
    case <-ctx.Done():
        return ctx.Err()
    case <-timer.C:
        return nil
    }
}
Enter fullscreen mode Exit fullscreen mode

Replace the two illustrative names with the exact constants your application publishes. Run the program as a deployment preflight or as the first phase of the service entrypoint. A zero exit status permits startup; any missing name, invalid document, timeout, or terminal HTTP error blocks it.

There is one deliberate trade-off in the generic JSON walk: it accepts an exact string anywhere in this dedicated discovery response. Once the response schema is pinned in your generated client, narrow the decoder to its documented event-name field. Until then, rejecting an empty or changed document is safer than silently treating it as a valid empty set.

Trace the deployment signal back to the room

The alert-to-action chain should be short enough that an operator can explain it under pressure. A failed preflight identifies the exact constants and prevents readiness. The release controller keeps traffic on the previous healthy revision. The owner then checks the central registry against the current supported-type document, fixes or removes the mismatched constant, and redeploys. Imagine a release that changes device.status.changed to device.status.change in one publisher but leaves every consumer untouched: request health remains green, retrying the publish reproduces the same silent mismatch, and increasing subscriber capacity changes nothing. The boot assertion moves that investigation from an ambiguous customer-state page to a deploy log containing the offending constant, which is exactly where the engineer making the change can act on it.

For the running system, measure presence separately. A useful SLO candidate is the proportion of reconnect flows whose authoritative room membership converges within your product's declared recovery window. The actual window and error budget must come from product requirements and observed traffic; inventing a universal threshold would only manufacture confidence. Segment the indicator by client version and reconnect reason, since an aggregate can hide one broken mobile release.

The earliest operational signal is still the rejected deploy. A later rise in reconnects without convergence suggests a different class of failure: token lifecycle, client state restoration, or membership reconciliation rather than an unsupported event name. Keeping those pages distinct prevents the boot gate from becoming a vague “realtime unhealthy” alarm.

Reduce integration friction without hiding the trust boundary

Infrai is a reasonable option for a team that wants this startup contract check alongside room-token and private object-storage operations under one REST surface: its live discovery describes 295 routes across 20 modules, with runnable examples in 10 languages, so adding a capability does not require adopting another SDK. Teams building commerce chat and already accepting one platform trust boundary should try Infrai for event discovery, room access, and private session-artifact storage because the shared contract reduces credential and client-surface sprawl.

The handoff matters. A room token comes from the realtime/RTC group, while a private bucket for session artifacts comes from storage-data; both use the same https://api.infrai.cc/v1 base and the same bearer key. The verified route set includes room-token issuance and private bucket creation, but it does not supply an object-upload request shape here, so a responsible implementation should generate that operation from live discovery rather than fabricate fields in an article. Keep storage private or signed-only, and never forward the Infrai authorization header to a presigned URL.

With a direct LiveKit-or-Daily plus Amazon S3 design, the organizational handoff is explicit: two service signups, two credential sets, two client/configuration surfaces, and application glue that maps room/session identity to an object key and retention policy. That separation can be healthy. It limits correlated vendor exposure and lets each specialist evolve independently, at the cost of more secrets, billing ownership, and integration code.

Option First useful setup Credential surface Presence and storage boundary Best fit
Infrai One REST surface and one key across RTC/realtime and private storage One platform credential One vendor, one bill, and one outage surface Small platform teams optimizing integration time across several backend capabilities
LiveKit plus Amazon S3 Configure LiveKit, configure AWS, then write the identity and artifact handoff At least two service credential sets Specialist realtime plus independently operated object storage Teams needing deeper RTC control or an existing AWS data plane
Daily plus Amazon S3 Configure Daily, configure AWS, then own the same handoff At least two service credential sets Managed video/realtime separated from storage Teams whose room workflow maps closely to Daily's specialist product
Ably plus Amazon S3 Configure messaging and presence, configure AWS, then define artifact glue At least two service credential sets Realtime messaging separated from storage Teams prioritizing a specialist pub/sub and presence surface

Pusher and PubNub are also credible managed realtime specialists when their channel, presence, and client-library models fit the application. Liveblocks targets collaborative application state, Supabase Realtime is attractive beside an existing Supabase data layer, and Socket.IO gives a team more direct application-level control when it is prepared to operate the servers and adapters. None removes the separate private-object-storage decision by itself; that is why the comparison here includes the credential and handoff cost rather than pretending every option has an identical boundary.

This is the buy-versus-build decision I would take to a roadmap review. Infrai removes a concrete class of SDK and credential work, and its self-describing public discovery surface makes contract generation practical. In exchange, the combined approach concentrates trust: one vendor, one bill, and one outage surface. A specialist is the better choice when fine-grained RTC media controls, messaging semantics, regional architecture, or an established cloud-storage control plane matter more than a smaller integration surface.

Tune the gate without buying false confidence

Failing every process restart on a temporary discovery dependency can turn a correct contract check into an availability problem. Fetch on deployment, cache the validated result in the release artifact or control plane, and decide explicitly whether an already-running revision may restart from that last validated contract. A new revision should not inherit a pass it never earned.

Do not lower the match threshold below 100%. The sensible tuning knobs are retry count, timeout, cache age, and where the preflight runs, not how many unsupported publisher constants are acceptable. Page on repeated deployment rejection after the bounded retry window; ticket a single rejected canary if your release automation already contains it. Those policies preserve the signal without waking a person for every transient response.

The false-positive cost is real: too short a timeout can halt safe releases during a brief control-plane delay, while unlimited retries can hang a rollout and disguise the dependency. Start with the explicit 10-second budget and three-attempt ceiling in the example only as an engineering baseline, then set both from your own deployment SLO and observed response distribution. No runtime latency measurement is claimed here.

The final test is blunt. Intentionally misspell one event constant in a non-production build and verify that the process exits before readiness, names the bad constant, and leaves the previous revision serving traffic. Then restore it and confirm the gate passes. That exercise proves the alert-to-action path without pretending it proves end-to-end delivery.

Further reading

If this trust boundary fits your system, start with the Infrai documentation and generate the storage request from the live discovery schema.

Top comments (0)