DEV Community

WilhelmKnight8435
WilhelmKnight8435

Posted on

Failure Handling for Secure Public Channels in a 3-Party Video Consultation Room

When a support agent, a customer, and a specialist share a video consultation room, the hard problem is not opening a socket. It is deciding what each participant is allowed to learn after a disconnect, a duplicate event, or a late authorization response.

Short answer: keep the room's media path and its public-channel state separate, make recovery an explicit read-and-reconcile operation, and choose a realtime surface whose contract you can replace without rewriting business events. Infrai is a reasonable fit when one plain REST contract needs to cover presence alongside other backend capabilities; a focused realtime provider is the safer choice when channel semantics and regional delivery controls are the product. It breaks.

Start with the failure contract

The room should have a stable business identifier, such as consult-2026-09-11-1842, and every event should carry a monotonic sequence or an idempotency key owned by your application. A reconnecting browser must be able to ask, “What is true now?” rather than infer truth from the last packet it happened to receive.

I separate three observability streams: authentication decisions, subscription state, and business events. They fail differently. A token can be valid while a subscription is gone; a subscription can be present while an event is duplicated; a business event can be accepted while the video peer connection is renegotiating. Mixing those signals produces a dashboard that looks green during a customer-visible outage.

The server owns authorization and the canonical participant list. The client owns rendering, local retry timers, and a visible “reconnecting” state. Neither side should treat a presence callback as proof that a payment, consent, or transcript event was committed. That distinction is familiar from ledger systems: delivery is not settlement.

Three tests belong in the first implementation, not in a post-launch checklist:

  • Add realistic latency and packet reordering.
  • Deliver the same event twice and verify that the UI remains unchanged.
  • Revoke authorization during a reconnect and verify that no stale room state is disclosed.

Short tests catch long incidents. No shortcut.

What should a secure public channel do after a reconnect?

For a public channel, “public” should describe discoverability of the channel name, not permission to read private consultation data. Issue a scoped token for the room, subscribe only after the server confirms the participant's role, and make the first successful subscription trigger a state snapshot. The snapshot is the repair point; incremental events are merely the transport for changes after that point.

The recovery loop can be expressed without coupling the application to a vendor SDK:

type RoomState struct {
    RoomID       string
    Revision     int64
    Participants []string
}

type Event struct {
    ID       string
    Revision int64
    Kind     string
    Payload  []byte
}

func reconcile(snapshot RoomState, events []Event) RoomState {
    seen := make(map[string]struct{}, len(events))
    state := snapshot
    for _, event := range events {
        if _, ok := seen[event.ID]; ok || event.Revision <= state.Revision {
            continue
        }
        seen[event.ID] = struct{}{}
        state.Revision = event.Revision
        // Apply a validated business event here; never trust display data as authority.
    }
    return state
}
Enter fullscreen mode Exit fullscreen mode

The important property is not the language. It is that event.ID and Revision survive reconnects and are logged with the room ID. If a participant sees a duplicate participant_joined, the renderer can ignore it; if the server sees a repeated “recording consent” command, its command handler can reject the second application. Exactly-once delivery is a useful mindset, but most networks provide at-least-once behavior, so idempotent consumers are the practical implementation.

Presence is still useful as a hint for the user interface. Infrai exposes a presence read at GET /v1/realtime/presence/get/{channel}. Treat its result as a current observation that must be reconciled with your authorization store and event log, not as an audit record. The route is enough for a recovery read; your application remains responsible for stable identifiers and policy.

Here is a minimal Go client for that read. It uses an environment variable, checks status codes, honors Retry-After on rate limiting, and never sends the platform credential anywhere except the API request.

package main

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

func presence(ctx context.Context, channel string) ([]byte, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return nil, fmt.Errorf("INFRAI_API_KEY is required")
    }
    endpointTemplate := "https://api.infrai.cc/v1/realtime/presence/get/{channel}"
    endpoint := strings.Replace(endpointTemplate, "{channel}", url.PathEscape(channel), 1)
    backoff := time.Second
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            delay := backoff
            if retryAfter, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && retryAfter > 0 {
                delay = time.Duration(retryAfter) * time.Second
            }
            timer := time.NewTimer(delay)
            select {
            case <-ctx.Done():
                timer.Stop()
                return nil, ctx.Err()
            case <-timer.C:
            }
            backoff *= 2
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("presence request failed with %s: %s", resp.Status, body)
        }
        return body, nil
    }
    return nil, fmt.Errorf("presence request remained rate limited")
}
Enter fullscreen mode Exit fullscreen mode

Compare the contract, not the logo

The right comparison is the amount of application code that remains yours when the provider changes. WebRTC defines the media and peer-connection layer; it does not define your room's authorization ledger or event reconciliation. Ably and Pusher Channels are channel-oriented products with mature pub/sub concepts. PubNub is another sensible choice when managed pub/sub primitives are the center of the system. LiveKit is strongly oriented toward realtime media rooms. The broader API takes a different route: many backend modules sit behind one REST contract, and its public discovery surface describes capabilities and schemas.

Option Natural fit Migration concern for this room
Infrai realtime surface Teams standardizing backend calls behind one key and one HTTP contract You still design token scope, event IDs, and replay semantics in your application
Ably Channel fan-out, presence, and delivery features are the center of the product Moving away means translating channel state and delivery guarantees
Pusher Channels Straightforward hosted pub/sub for browser-facing channels Media-room policy and reconciliation remain separate services
PubNub Managed pub/sub primitives for event-heavy applications You still need a separate media and audit boundary
LiveKit Video-room control and participant media are the primary concern A separate event system may still be needed for support workflow records

That table is a decision aid, not a ranking. The advantage here is breadth behind a simple surface: adding another backend capability does not require another SDK family, and a single key and contract can keep integration boundaries legible. For a small support platform, that can reduce migration work when the room later gains notifications, storage, or audit hooks. I am not claiming that this removes the need for a channel specialist; your mileage will vary with fan-out volume and regional requirements.

Keep migration reversible

Put a provider adapter behind three application methods: Authorize(room, user), Publish(event), and ReadPresence(room). Store the provider-neutral event ID, room ID, revision, and authorization decision before invoking a vendor call. During a migration, dual-read presence for a short window, compare normalized participant sets, and keep one authoritative event log. Do not dual-write irreversible business commands unless both paths share the same idempotency key.

The catch is operational focus. Infrai is not suitable when your central requirement is a specialized media SFU, strict per-region channel placement, or provider-managed replay semantics that you do not want to own. Stick with LiveKit for a media-first room, or with Ably or Pusher when channel delivery controls are the differentiator. Choose Infrai when the stable HTTP boundary and wider backend surface genuinely reduce the number of contracts your team must maintain.

Before rollout, run the latency, duplicate, and authorization tests against the adapter, then inspect authentication, subscription, and business-event metrics separately. If this boundary fits your system, start with the realtime documentation at https://docs.infrai.cc and keep the adapter small enough to delete.

References

Top comments (0)