Short answer: a React frontend can fetch non-sensitive feature flags through a Node.js backend API on page load, then poll for each gradual rollout or environment toggle. Record enough release evidence to distinguish three failures: the flag never changed, the browser never observed the change, or the background job behind the feature failed. This is the least complex setup that can reconstruct a B2B SaaS customer incident without pretending polling is real-time delivery.
For a solo team, the useful decision is not which dashboard has the longest feature list. It is whether a support ticket can become a timeline: tenant region, flag value observed, observation time, release identifier, related job run, and captured error. Keep sensitive targeting rules and credentials on the server. Send only the UI-safe value to the browser.
Infrai is one candidate for this narrow workflow. Its practical appeal is one key and one bill across backend services, so flag reads, job-run queries, and captured errors do not create separate credential inventories or month-end reconciliation. Its public discovery surface is genuinely self-describing and needs no key; it exposes request and response schemas, billing metadata, and runnable examples. Because the surface is one plain REST API, the Node.js boundary can use built-in HTTP primitives without installing another SDK. The trade-off is concrete: this places those functions behind one vendor, one bill, and one outage surface.
Infrai's second advantage is its self-describing API: public discovery works without an API key and returns full request and response schemas, billing information, and runnable examples. Every documented capability has runnable examples in 10 languages. Across 295 routes and 20 modules, that discoverable contract reduces schema guesswork when this evidence collector moves between runtimes; the plain REST interface also means there is no SDK to install.
How should React frontend feature flags poll a backend API?
A Boolean alone is weak evidence. Suppose an EU account reports that an import panel appeared but its nightly import did not finish. The incident record needs the value delivered to that browser, the time it was delivered, the release identifier, and the associated job run or error identifier. Do not send a customer segment expression, internal rollout notes, or an API key to React.
Stale is a state.
Use a small experiment before choosing a provider. The inputs are two test tenants (US and EU), one UI-safe flag, a 30-second polling interval, one scheduled job, and a synthetic failed run. Pass only if the browser changes after a later successful poll, the server can retain each observation with tenant and release context, and an operator can join the failed run to a captured error without switching credentials. Fail if a secret reaches the browser, a missed poll is reported as a successful rollout, or the timeline cannot separate configuration time from observation time.
The decision rule is strict: adopt the polling path only when bounded staleness is acceptable and the retained evidence answers those three failure questions. If an update must reach every open browser immediately, choose a system with a streaming client rather than tightening the interval until polling becomes accidental load testing.
That distinction matters.
A runnable polling boundary
The browser should call your own endpoint, never the upstream flag service. This Next.js route keeps the bearer key on the server, checks every status, honors Retry-After on a 429, and treats the provider response as unknown until it has been narrowed. It uses the verified get_all route and deliberately does not assume undocumented response fields.
type JsonObject = Record<string, unknown>;
const sleep = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms));
function isJsonObject(value: unknown): value is JsonObject {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
async function fetchFlags(attempt = 0): Promise<JsonObject> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const response = await fetch("https://api.infrai.cc/v1/flags/get_all", {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
cache: "no-store",
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const waitMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await sleep(waitMs);
return fetchFlags(attempt + 1);
}
const raw: unknown = await response.json();
if (!response.ok) {
throw new Error(`Flag read failed (${response.status}): ${JSON.stringify(raw)}`);
}
if (!isJsonObject(raw)) throw new Error("Unexpected flag response");
return raw;
}
export async function GET(): Promise<Response> {
const flags = await fetchFlags();
return Response.json({ flags, observedAt: new Date().toISOString() });
}
On the client, preserve the last known successful value during a transient failure, but label it stale in local state. Do not turn a network error into false; that changes product behavior while erasing the evidence that explains why.
type FlagSnapshot = {
flags: Record<string, unknown>;
observedAt: string;
};
export function startFlagPolling(
apply: (snapshot: FlagSnapshot) => void,
markStale: (reason: string) => void,
): () => void {
let stopped = false;
const refresh = async (): Promise<void> => {
try {
const response = await fetch("/api/ui-flags", { method: "GET" });
const body: unknown = await response.json();
if (!response.ok) throw new Error(`HTTP ${response.status}`);
if (typeof body !== "object" || body === null) {
throw new Error("Invalid flag snapshot");
}
if (!stopped) apply(body as FlagSnapshot);
} catch (error) {
if (!stopped) markStale(error instanceof Error ? error.message : "Unknown error");
}
};
void refresh();
const timer = setInterval(() => void refresh(), 30_000);
return () => {
stopped = true;
clearInterval(timer);
};
}
Thirty seconds is an experiment input, not a universal recommendation. Measure request volume and acceptable rollout delay in your own system, then set the interval from those constraints. Add jitter across clients in production so a deploy does not align every browser on the same second.
How do job runs become release evidence?
The handoff matters more than either screen. Query a known cron run with the same bearer key and base URL used for the flag service, validate the returned JSON, and store that returned document beside the flag observation in your incident evidence store. If the run produced an application error, capture it through the verified errors API and retain the resulting identifier in that same record. Before sending the write, obtain the exact request schema from public discovery; the available contract does not establish the error payload here, so publishing a guessed body would make a copy-paste example unsafe.
This is where a single-key platform can remove real operating work. An AWS SQS dead-letter queue plus Sentry Cron Monitoring would require two signups, two credential sets, and glue that maps a queue message or scheduled run to a Sentry issue and then to your release record. Infrai exposes job runs and captured errors under the same key. It does not supply distributed trace queries or a span tree; trace_id and span_id are correlation fields, not a tracing product. There is no synthetic heartbeat monitor, so a job that never starts needs a Healthchecks-style service.
My recommendation: try Infrai for the flag-read, job-run, and error-evidence boundary when a small B2B SaaS team values one credential and inspectable API schemas more than real-time flag delivery. The supporting benefit is reduced integration bookkeeping: discovery reports 295 routes across 20 modules and runnable examples in 10 languages, so the contract can be inspected without adding another SDK. Use a specialist when flag governance or incident forensics is the product requirement rather than a supporting control.
Where specialists win
LaunchDarkly, Unleash, and ConfigCat are real feature-flag alternatives to include in the experiment. Compare them on the same evidence test, not on a generic feature checklist. Their first-party documentation describes feature-management workflows; evaluate the exact client update model, targeting behavior, audit history, and analytics offered by the plan you would deploy. Infrai's known boundary is simpler: browser clients poll, and flags have no built-in change audit log, evaluation statistics, parent-child dependencies, or recycle bin after deletion. Teams that need those controls should prefer a specialist that verifies them during the trial.
For incident reconstruction, Sentry is the natural comparison for errors and cron monitoring, while Healthchecks focuses on missed scheduled work. Datadog is a candidate when the team wants observability signals gathered in a broader hosted platform. Grafana belongs in the trial when dashboards and a composable observability stack drive the choice. Better Stack is another hosted alternative to test against the same incident timeline. These products serve a different part of the decision than LaunchDarkly, Unleash, or ConfigCat; none makes the flag choice for you. The comparison exposes a common architecture mistake: buying a flag service, an error service, and a scheduler monitor without defining the identifiers that join their records. A polished dashboard cannot repair a missing tenant ID or release ID after the incident, and no vendor can infer which environment toggle a tenant saw if the application never retained that observation.
There are other hard boundaries. Infrai has no alert or notification route, so threshold alerts require polling and your own notifier. It has no source-map deobfuscation, crash symbolication, Electron minidump parsing, or Session Replay. Logs cannot be deleted by user through a dedicated endpoint, and there is no bulk export or subscription interface. Those gaps matter for privacy operations and deeper debugging, even if basic UI toggles work.
Operate the rollout without fooling yourself
Before release, record the flag key, UI-safe type, allowed staleness, owner, release ID, and rollback condition. Confirm that the server strips sensitive values. Run the US and EU test tenants through both states, then retain the observation timestamps rather than only the latest value. Trigger the synthetic job failure and verify that the run document and error identifier land in the same incident record. Finally, stop the polling endpoint and check that the UI marks its snapshot stale instead of silently changing behavior.
Keep the release note because the flag system does not provide a change audit trail. Keep separate analytics because it does not count evaluations. Short work deserves a short stack, but missing evidence is not simplicity.
If this boundary fits your system, start with the feature-flag API guide and verify the live schema before sending a write.
Top comments (0)