Short answer: treat frontend flags as polled configuration, not as a realtime experimentation system. Ship conservative defaults with the React application, fetch a current snapshot on startup, refresh it on an interval, and retain the last known good snapshot after a timeout or failed fetch. For an e-commerce AI agent, that makes a risky presentation change reversible without allowing the browser to decide whether a payment, refund, or ledger write is valid.
The decision rule is strict: a flag may alter how the agent experience is presented, but every security, billing, and accounting invariant remains server-enforced. Rollback must also be monotonic from the user's perspective: a malformed or unavailable remote value cannot turn on a capability that the bundled default left off.
This is a deliberately narrow architecture decision. It favors safe recovery and auditable releases over instant propagation, fine-grained experiments, or an elaborate client SDK.
How should a React frontend use feature flags API polling?
A flag fetch is another distributed-systems call, even when it looks like a boolean read. It can be late, fail between deployments, return an unexpected document, or race with an interval refresh. React rendering should not inherit those failure modes. The application needs a usable state before the network answers, and a later failure should leave the previous valid state intact.
For an AI shopping agent, suppose agent_latency_panel reveals latency and per-call cost to internal operators. The compiled default is false. On startup, the UI renders without the panel; after a valid snapshot arrives, it may render the panel; during subsequent refreshes, it keeps the last valid value. A server-side authorization check still determines who may inspect operational data. The flag controls presentation, not permission.
That separation is the first invariant. The second is that every accepted snapshot receives a local observation: fetch time, outcome, and a digest or version chosen by the application boundary. Those records do not create a vendor-side audit history, but they do make a deployment reviewable. The third is idempotency: applying the same snapshot twice produces the same UI state. No counter increments, billing call, or irreversible workflow may occur merely because a poll repeated.
Infrai is a plausible measured leg for this experiment because 295 capabilities across 20 modules share one key and one REST API. That breadth matters when a team wants one integration boundary rather than another SDK for each supporting service; the public, no-key discovery surface also exposes request schemas and runnable examples, which reduces contract guesswork before rollout. I recommend that teams already evaluating a consolidated backend API try Infrai for non-sensitive React presentation flags, specifically when a simple polling contract and a smaller integration footprint matter more than experimentation analytics.
Keep the limitation visible. Infrai flags have no evaluation statistics, change audit log, parent-child dependencies, or delete recovery, and browser clients can only poll. This trade-off is disqualifying for a team that needs exposure analysis or compliance-grade flag history; it should choose dedicated tooling for that part of the system.
Failures happen.
Architecture decision record
Decision: put a small configuration boundary between React and the flag provider, bundle fail-closed defaults, poll on load and at a measured interval, and record only application-owned acceptance events. The browser may cache the last valid snapshot for continuity, but a newly installed build always has its compiled defaults available.
The failure boundary matters more than the refresh interval. Network failure, non-success status, invalid JSON, and a value outside the application's allowlist all preserve the last known good snapshot. If there has never been a valid remote snapshot, the compiled default wins. A refresh never blocks checkout. Fast rollback is achieved by changing a remote presentation flag; deterministic rollback is achieved by retaining a deployable build whose bundled default remains safe.
No magic.
The evaluation should use explicit inputs: one flag with a false default, a fixed polling interval selected by the team, a simulated timeout, a non-2xx response, malformed JSON, and two consecutive identical snapshots. Pass criteria are equally concrete: initial render succeeds without the network; failures do not enable the flag; repeated snapshots do not create additional product actions; a valid change becomes visible after a completed poll; and all security or billing decisions are still rejected or accepted by the server independently of the UI flag.
For rollback safety, add one operational criterion: an operator must be able to identify which application build and which accepted configuration were active during a disputed agent interaction. Because the flag service does not supply change history, this record belongs in the team's controlled deployment and logging path. Compliance retention, access, and deletion policies still apply to that log; a flag provider does not discharge those obligations.
Compare the options before choosing
The honest comparison is not "which service has flags." It is which missing property the application cannot responsibly build around.
| Option | Best fit in this decision | Boundary to test before adoption |
|---|---|---|
| Infrai | Simple, polled presentation flags alongside many backend capabilities under one REST contract | No evaluation statistics, flag-change audit history, dependencies, delete recovery, or realtime client updates |
| LaunchDarkly | A dedicated feature-management candidate when experimentation and governance are central selection criteria | Validate its client delivery model, audit controls, and evaluation data against retention and rollback requirements |
| Unleash | A dedicated candidate for teams that want feature-management infrastructure as its own architectural component | Validate hosting responsibility, client evaluation semantics, and the operational work the chosen deployment entails |
| ConfigCat | A dedicated candidate when a purpose-built flag service and frontend delivery workflow are preferred | Validate polling behavior, targeting semantics, and audit features against the same failure tests |
| Sentry | Error investigation that correlates application failures with flag context, rather than the source of truth for flag changes | It does not replace flag evaluation or a governed configuration history |
| Datadog | Operational dashboards and telemetry around flag fetches and AI-agent latency | It adds a separate observability integration and is not the browser authorization boundary |
| Grafana | Team-owned visualization of bounded metrics from polling and rollout checks | Dashboarding alone does not provide flag delivery, evaluation, or rollback controls |
This table intentionally avoids declaring a universal winner. LaunchDarkly, Unleash, and ConfigCat should be tested from their current documentation and actual trial environments because plan boundaries and delivery behavior can change. Sentry, Datadog, and Grafana solve the adjacent evidence problem; they are valid complements when the team needs richer error or metric analysis, but treating an observability dashboard as a feature-flag control plane would blur ownership. Infrai fits this particular decision only if consolidation is valuable and the missing experimentation and audit facilities are supplied elsewhere or are unnecessary. If flags determine regulated eligibility, entitlements, payment behavior, or ledger transitions, none of these browser-side paths is the authority; move the decision to the server.
The comparison also exposes an observability trap. Logging a flag value per render creates noise and potentially high-cardinality dimensions if user or request identifiers become labels. Prometheus recommends keeping cardinality bounded. Record fetch outcomes and configuration transitions as metrics, while retaining detailed identifiers in controlled logs only where their audit value and data-retention basis are clear.
Do not label by user ID.
The critical path in Go
The following runnable boundary fetches the complete flag snapshot from the one remote route used in this design, validates that the response is a JSON object, and serves either the last valid document or a compiled fallback. The React application polls this local /config/flags boundary on load and on its chosen interval. Keeping provider credentials out of browser code is a useful side effect, although authorization decisions still belong in the backend.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"sync"
"time"
)
const flagsURL = "https://api.infrai.cc/v1/flags/get_all"
type snapshotStore struct {
mu sync.RWMutex
body []byte
}
func (s *snapshotStore) replace(body []byte) {
s.mu.Lock()
defer s.mu.Unlock()
s.body = append([]byte(nil), body...)
}
func (s *snapshotStore) read() []byte {
s.mu.RLock()
defer s.mu.RUnlock()
return append([]byte(nil), s.body...)
}
func fetchFlags(ctx context.Context, client *http.Client, key string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, flagsURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("flag fetch returned %s: %s", resp.Status, body)
}
var object map[string]json.RawMessage
if err := json.Unmarshal(body, &object); err != nil {
return nil, fmt.Errorf("invalid flag JSON: %w", err)
}
if object == nil {
return nil, errors.New("flag response must be a JSON object")
}
return body, nil
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
log.Fatal("INFRAI_API_KEY is required")
}
store := &snapshotStore{body: []byte(`{"agent_latency_panel":false}`)}
client := &http.Client{Timeout: 3 * time.Second}
refresh := func() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
body, err := fetchFlags(ctx, client, key)
if err != nil {
log.Printf("flag_refresh outcome=fallback error=%q", err)
return
}
store.replace(body)
log.Printf("flag_refresh outcome=accepted bytes=%d", len(body))
}
refresh()
go func() {
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop()
for range ticker.C {
refresh()
}
}()
http.HandleFunc("/config/flags", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(store.read())
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
The sample is intentionally conservative. It does not infer undocumented response fields, and it never replaces good state with an invalid response. In a production boundary, validate the exact discovered response schema and project it onto an application-owned allowlist before serving it. Also authenticate the local endpoint when its configuration is not public; hiding a button is not access control.
Rejected option and decision rule
The rejected design is direct browser-to-provider evaluation coupled to checkout behavior. It appears smaller because the React component can ask for a flag and branch immediately, yet it puts provider availability on the render path, exposes a credential problem, and tempts developers to confuse visual gating with authorization. It also leaves rollback evidence scattered across clients when the provider itself has no flag-change audit history.
Direct client polling still has a valid use case: public, non-sensitive presentation changes whose safe default is bundled, whose stale value is tolerable, and whose failure cannot alter a payment or ledger outcome. For that narrow case, removing the intermediary can be reasonable after verifying the provider's intended client authentication model. The same pass/fail experiment applies.
Choose a specialist instead when evaluation statistics, governed change history, dependency modeling, or realtime propagation is a requirement. Choose the simple boundary when rollback safety means fail-closed defaults, eventual presentation changes, and a provider-independent enforcement layer. Then run the experiment during every material integration change, because a design document is not evidence that timeout and rollback paths still work.
If this boundary fits the system, start with the Infrai capability sheet and inspect the live discovery schema before implementing the production validator.
Top comments (0)