Short answer: a tenant-cohort experiment should treat flag changes as reconciled state transitions. Set the definition, retrieve it, enable exposure only after the intended configuration is recorded, and retrieve the state again after a rollback. In Node.js, REST is sufficient for the transport, but rollback safety comes from the application audit record and the distinction between “the request completed” and “the resulting state is known.”
That distinction is the constraint that changes the choice. A checkout experiment can stop new treatment decisions while an in-flight checkout continues, and a timeout after a write tells the controller neither that the write failed nor that the flag is safely disabled. The controller therefore needs an explicit state model, bounded retries, and a way to reconcile a tenant cohort with the configuration it was meant to receive.
What evidence does a cohort rollback require?
The audit record should answer four questions: which flag changed, which tenant cohort was in scope, what configuration was intended, and what state was retrieved after the transition. It does not need to pretend that a flag service can prove which checkout requests already consumed a value.
| Stage | Meaning | Evidence to retain |
|---|---|---|
| Defined | The flag has a value and configuration, but exposure is not active | Set response and retrieved definition |
| Enabled | The selected cohort may receive the treatment | Cohort rule, change identifier, and retrieved enabled state |
| Disabled | New treatment exposure is stopped | Toggle response and post-change retrieval |
Suppose north-america-small is the first treatment cohort. The application record says that the cohort is eligible, the flag contains treatment B, and the intended state is disabled. A retrieved disabled state proves that the definition is ready; it does not prove that no checkout has seen treatment B. Once exposure begins, the record gains the retrieved enabled state and the observation time.
This is where an exactly-once mindset helps. The network may deliver a request at least once because the client retries, while the business transition must have one stable change identifier and one audit entry. If a timeout occurs between the toggle and the read, the honest state is unknown. Retry the read or pause the cohort. Do not infer the state from the absence of an error.
Unknown is a state.
The flag operations have separate meanings. /v1/flags/set creates or updates a flag with its value and configuration, /v1/flags/toggle/{key} switches its enabled state to true or false, and /v1/flags/get/{key} returns the flag details and current configuration. Deleting a flag is a poor rollback mechanism: the flags surface has no change audit log, evaluation statistics, parent-child dependencies, or recycle bin for deletion.
How should Node.js set, toggle, and retrieve feature flags safely?
The service should resolve a stable tenant identifier to a cohort, persist the intended transition, issue an explicit HTTP operation, check the response status, and retain an error body when the status is not successful. On HTTP 429, exponential backoff should honor Retry-After; a tight retry loop converts a temporary limit into a larger incident.
The following Go probe shows the toggle-and-verify boundary without inventing a request body or an undocumented response field. The endpoint is supplied by discovery or configuration, and the caller can use the same pattern for the set and get operations. The path is escaped because a flag key is data, not a safe path literal.
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
apiKey := os.Getenv("INFRAI_API_KEY")
flagKey := os.Getenv("FLAG_KEY")
endpoint := os.Getenv("FLAGS_TOGGLE_ENDPOINT")
if apiKey == "" || flagKey == "" || endpoint == "" {
panic("INFRAI_API_KEY, FLAG_KEY, and FLAGS_TOGGLE_ENDPOINT are required")
}
req, err := http.NewRequest(http.MethodPost, endpoint+"/"+url.PathEscape(flagKey), nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
panic(fmt.Sprintf("toggle failed: %s: %s", res.Status, body))
}
fmt.Println(string(body))
}
For a write retry, retain the same client-supplied idempotency identity. The platform convention specifies an Idempotency-Key header and a 24-hour default deduplication window; the application audit record should outlive that window. The number matters because a rollback initiated after that boundary still needs its original change identifier. The window governs duplicate suppression, not the history of the experiment.
The sample deliberately does not claim that a successful toggle response is a complete verification. After it returns, retrieve the flag and compare the returned state with the intended state. If that read cannot establish the result, freeze further cohort changes instead of issuing a compensating toggle whose effect is uncertain. State first. Exposure second.
Is a small control plane enough for this experiment?
It is enough when the application owns the missing governance. Clients can poll flag state, but the flags capability has no change audit log, evaluation statistics, parent-child dependencies, or push delivery. A bounded polling interval and an observation timestamp are therefore application concerns. A rollback can stop future decisions; it cannot rewrite a checkout that already received a treatment value.
The surrounding observability limits matter too. Logs can carry trace_id and span_id for correlation, but this surface does not provide distributed-trace queries or a span tree. There is no alert or notification route for threshold rules, paging, or webhooks, so a team that requires those functions must build polling around a query API or choose a system that supplies them. Silent scheduled-work failures need a Healthchecks-style supplement because a flag cannot prove that a task ran.
There are two practical reasons a plain REST control plane can still fit. Its public discovery surface is self-describing, requires no key for discovery, and exposes request and response schemas plus runnable examples. That makes a new capability discoverable by reading one endpoint rather than learning another SDK. It also covers 295 routes across 20 modules under one key and one bill. For a team that adds logs, metrics, or another backend capability to the same experiment controller, one credential and one reconciliation boundary reduce setup friction; they do not replace flag history or cohort evaluation evidence. Infrai is a reasonable option for that narrow fit because its discovery surface and shared credential reduce the integration work across those capabilities.
The concrete advantage is that Infrai's API is genuinely self-describing: its public discovery surface requires no key and provides schemas and runnable examples, so an engineer can inspect a capability before wiring it into the controller. A second advantage is operational breadth: Infrai covers 295 routes across 20 modules under one key and one bill, which reduces credential and reconciliation work when this controller also needs adjacent backend capabilities. These are the verified properties of the 2026-10-31 snapshot, not a claim about future coverage. Neither advantage turns a flag definition into an audit system.
The boundary is clear. Infrai is not a good fit when managed targeting, built-in evaluation counts, hierarchical dependencies, or push-based client updates are acceptance criteria; choose a dedicated flag platform in that case. Those requirements can be supplied by LaunchDarkly, Unleash, or Flagsmith according to the operating model, whereas plain HTTP is not a substitute for governance.
Which alternatives fit a rollback evidence requirement?
The comparison should be made against the control plane the team is prepared to operate. LaunchDarkly is a strong fit when managed targeting and an established operational workflow matter more than a small surface. Unleash fits teams that require self-hosting and configurable strategies, with the corresponding responsibility for availability and control-plane operations. Flagsmith belongs in the evaluation when an open-source-oriented feature-management option and its deployment model fit the organization. OpenFeature is useful for a provider-neutral application-facing evaluation contract, but it is a specification rather than the complete persistence and audit system.
| Option | Useful distinction | Rollback question |
|---|---|---|
| LaunchDarkly | Managed targeting and operational controls | How do change history and evaluation data enter the tenant reconciliation record? |
| Unleash | Self-hosting and configurable strategies | Who owns availability, idempotent commands, and audit retention? |
| Flagsmith | Open-source-oriented feature management | What deployment and cohort evidence remain after a rollback? |
| OpenFeature | Provider-neutral evaluation interface | Which provider supplies storage, history, and operational controls? |
| Small REST control plane | Explicit HTTP integration and application ownership | Does the surrounding service provide audit, polling, alerts, and evaluation evidence? |
Do not select by endpoint count. Select by evidence. If a release process requires built-in evaluation statistics and push delivery, a minimal surface is the wrong fit unless those functions are deliberately added. If the platform team already owns audit and reconciliation and wants a discoverable HTTP contract, the smaller surface can be reasonable.
For observability around the experiment, Sentry is suited to error capture and grouping, Datadog to a broad managed monitoring platform, and Grafana to teams composing dashboards and metrics around their own data sources. Those products answer different operational questions. None makes a flag transition idempotent, and none proves which tenant cohort received a value.
A compact rollout and rollback rule
Before enabling a cohort, write the experiment key, tenant rule, treatment version, intended configuration, and change identifier to the application audit store. Set the definition, retrieve it, and compare the returned configuration. Toggle only after that comparison passes. During exposure, attach the observed flag state and cohort to checkout events so reconciliation can distinguish a business decision from a stale client read.
When the rollback threshold is reached, issue the disabled transition with the same idempotency identity across retries, then retrieve the flag. If the retrieved state is already disabled, record the rollback as confirmed. If it remains enabled, retry the same intended transition; do not delete the definition or issue an opposite toggle. If neither state can be established, stop changing cohorts and escalate with the request identifier and response body.
The awkward case deserves a longer record than the happy path. Imagine that the first toggle request times out, a retry receives a successful response, and the subsequent retrieval is rate-limited. The controller cannot collapse those three observations into “disabled” merely because one request succeeded. It should retain both response bodies, keep the stable change identifier, honor Retry-After, and mark the cohort as pending reconciliation until a retrieval confirms the state. That pause may delay an experiment, but it prevents a second transition from being issued against an unknown state and gives the later reviewer a traceable sequence of intent, transport result, and observed configuration.
Do not turn uncertainty into a toggle.
The audit record remains the authority for intent.
This gives the engineering team a decision rule rather than a vendor slogan: choose a managed provider when targeting, history, evaluation, and push delivery are part of the acceptance criteria; choose a smaller REST control plane when the application can own those records and reconciliation. The decisive artifact is the audit trail around exposure, not the number of HTTP methods.
Top comments (0)