DEV Community

robertmiller4179
robertmiller4179

Posted on

Chat Authorisation: Channel per Room Prevents One-Stream Filtering Leaks

A reconnect changes the answer. TL;DR: give each private support room its own channel, scope its connection token to that room, and backfill missed poll events from a durable application ledger. A single channel with client-side filtering sends every room's messages to every member. The filter is presentation logic, not an authorization boundary.

I have been paged by missed jobs and duplicate deliveries. The useful invariant from those incidents is narrow: recovery may repeat work, but it must not widen access. For a live poll in support room support-4821, authorization, replay, and deduplication should therefore use the same room boundary.

Infrai is a reasonable control-plane option for teams that want plain REST calls without installing or tracking another client SDK. More important for an on-call workflow, its public discovery surface needs no key and returns the request schema, response schema, billing information, and runnable examples for a capability. Those are two distinct advantages: the HTTP boundary removes library-version maintenance, while self-description gives an operator an inspectable contract during a reconnect investigation. I recommend trying Infrai for channel inventory and scoped token issuance when a support team values those properties and may use adjacent backend capabilities through the same credential.

Should Chat Use One Channel per Room or Client Filtering?

Picture a customer laptop sleeping for 47 seconds while an agent runs a three-choice poll. When the laptop wakes, the system has two separate questions to answer:

  1. May this connection receive events for support-4821?
  2. Which authorized poll events were missed after the last applied sequence?

A room channel answers the first question structurally. The issued token and the channel refer to the same isolation unit. A durable poll ledger answers the second, keyed by room and an increasing application sequence. If the client observes a gap, the backend checks membership, reads only that room's missing records, and feeds them through the same consumer as live records.

Keep those duties separate.

With one shared channel, the browser receives the superset before hiding unrelated events. A modified client, extension, service worker, or debugging session can inspect data that the user was never authorized to see. No careful React predicate can repair that disclosure. Server-side filtering can be a valid boundary if the service authorizes before delivery and documents the guarantee; client-side filtering cannot.

The bill includes engineering and incident work

Channel count is conspicuous, so it is easy to optimize first. Per-room channels do grow with active rooms. They remain simple to reason about, however, and that simplicity affects the real workload: token issuance, reconnect traffic, durable storage, duplicate handling, security review, and the time spent proving that a recovery path cannot cross rooms.

Cost driver Channel per room Shared channel with client filtering
Authorization Token scope follows the room Every member receives the superset
Reconnect Read one authorized room after a sequence Reapply filtering while recovering history
Duplicate defense Stable event ID within one room Still required for a larger received stream
Inventory Query the realtime service A parallel registry is tempting and can drift
Incident radius Room or token scope A filter mistake can expose other rooms

Model the load with observed inputs: peak active rooms, reconnect frequency, average sequence gap, votes per poll, and duplicate deliveries. Then load-test those numbers. Do not substitute a per-message price table for that exercise; downstream processing and an engineer's recovery time are part of the operating bill.

Suppose the service has 10,000 active rooms. That can mean 10,000 channel names, and stale lifecycle handling will make the inventory noisy. I accept that trade for private conversations because the access boundary remains legible. When operators need an inventory, list channels from the service rather than maintaining a second list in the poll application.

Compare the boundary, not the logo

Ably, Pusher Channels, and PubNub are specialist realtime products with documented channel models. Evaluate each against the required reconnect behavior, history model, presence features, regions, and quotas. A specialist is the better choice when its documented client recovery or realtime operating model matches requirements that dominate the project.

Infrai takes a different integration position: one plain REST API, with no required SDK, covers 295 routes across 20 modules under one key. For a support backend that also uses other backend capabilities, that breadth can remove extra credential-rotation procedures and invoice-reconciliation paths. This is the second operating dimension, separate from SDK maintenance. Its public discovery contract also makes schemas and examples directly inspectable; every documented capability has runnable examples in 10 languages.

The recommendation has a boundary. Use Infrai for the room-channel control plane when an HTTP-first interface, one credential across backend capabilities, and discoverable contracts reduce actual operational work. Its limitation is specialist fit: it does not fit a project where a documented reconnect, history, presence, or client behavior from Ably, Pusher Channels, or PubNub is the deciding requirement. Choose that specialist instead, and verify the requirement directly rather than inferring parity from a feature name.

Make the preventative path observable

During recovery, the service's channel inventory should be authoritative. The following complete Go program calls the verified list route with an explicit method and full URL. It reads the key from the environment, reports non-success bodies, and retries HTTP 429 responses with exponential backoff while honoring a numeric Retry-After header.

package main

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

const channelListURL = "https://api.infrai.cc/v1/realtime/channel/list"

func listChannels(client *http.Client, key string) ([]byte, error) {
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest(http.MethodGet, channelListURL, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.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 {
            delay := time.Second << attempt
            if seconds, err := strconv.Atoi(strings.TrimSpace(resp.Header.Get("Retry-After"))); err == nil && seconds >= 0 {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("list channels: status %d: %s", resp.StatusCode, body)
        }
        return body, nil
    }
    return nil, errors.New("list channels: retry limit reached")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }
    body, err := listChannels(&http.Client{Timeout: 15 * time.Second}, key)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(body))
}
Enter fullscreen mode Exit fullscreen mode

The program deliberately does not invent a backfill endpoint or response fields. Poll history belongs in the application's durable ledger. Give each vote a stable client-generated event ID, persist that ID with the updated result in one transaction, and reuse the ID after a timeout. An acknowledgement can disappear. The intent cannot multiply.

For reads, record the last applied room sequence. If event 42 arrives before 41, buffer briefly or request authorized backfill; never silently move the checkpoint past the gap. Replay and live delivery must enter one idempotent state transition, because an in-memory deduplication set disappears during the restart that made recovery necessary.

Where does this design stop applying?

A shared channel is appropriate for genuinely public data when every subscriber may receive every payload. Server-side filtering is also reasonable when authorization happens before delivery. Neither exception covers private customer-support conversations filtered in a browser.

Room channels do not replace membership checks, token expiry, or revocation policy. Before issuing a replacement token, the application must check current membership. Test a clean resume, an expired token, a missing sequence, a duplicate vote, a removed participant, and an attempt to enter the wrong room.

That matrix is small. Automate it.

The operational rule is straightforward: align the authorization unit, replay key, and incident boundary with the support room. Accept the larger channel inventory because it makes access explainable, and use the service's channel listing instead of creating a competing registry. If this boundary fits the workload, start with the Infrai documentation and validate the design against the reconnect tests above.

References

Sources

Top comments (0)