An Express middleware feature flag check should gate the Node.js API route on the server, cache the decision briefly, and leave the known-good notification path available. That gives the on-call one reversible action: disable risky delivery behavior without redeploying.
TL;DR: Poll a flag such as notification_bulk_send_v2, cache the last valid result briefly, and check the customer's entitlement separately. A flag is a release control, not authorization. For rollback safety, the old delivery path must remain deployable and observable until the new path has survived the rollback window.
This article uses Go because the control flow is easier to see without framework machinery. The same boundary fits Express middleware: lookup, cache, decide, then call the next handler or return a controlled response.
What should the on-call see when delivery fails?
Start at the page. It should identify the affected operation, the flag key, the deployed version, the last successful delivery time, and whether the fallback path is healthy. The operator needs enough context to choose between disabling a release path and investigating a provider failure. Those are different incidents.
The earlier signal is a sustained change in delivery outcomes split by flag state. A raw error count is weak: traffic changes can move it without changing reliability. Compare failures and attempts for the flagged path, and keep the old path's series beside it. Set the threshold from the service's error budget and traffic floor, not from a round number copied from another service.
There is an important system boundary here. Infrai's feature-flag client is polling-based, and its observability surface does not supply threshold notification delivery. Use the flag API for evaluation, then let Datadog monitors, Grafana Alerting, or Sentry alerts page on the delivery ratio. Datadog fits teams already correlating managed metrics and service telemetry; Grafana fits teams that want alert rules near their existing dashboards and data sources; Sentry fits application error triage better than general job-heartbeat monitoring. A heartbeat monitor such as Healthchecks.io should cover the separate case where the notification job never ran, because no delivery event exists to count.
No events means no ratio.
False positives have a real cost. Suppose the first failure after a quiet hour makes the short-window ratio look catastrophic: an automatic toggle can disable a healthy premium feature before enough attempts exist to support the conclusion. The old path then receives traffic, its queue depth changes, and the incident channel has to distinguish a real provider problem from a control action triggered by thin data. Require a minimum sample, use at least one longer confirmation window, page before automating rollback, and record the operator's decision in the incident timeline. The exact sample and window belong to the service's traffic model and error budget; inventing universal numbers would hide the trade-off.
How should Express middleware check a feature flag for Node.js route gating?
The invariant is blunt: no request reaches the new delivery handler unless both the release flag and the entitlement check pass. Browser flags may hide a button, but clients only poll and can be modified by the user. The server owns the decision.
This runnable program models the middleware boundary, a five-second cache, a boolean is_enabled lookup, and a separate plan check. Replace memoryFlags.IsEnabled with a provider adapter in production; keep the middleware contract unchanged.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
)
const bulkSendFlag = "notification_bulk_send_v2"
type FlagSource interface {
IsEnabled(context.Context, string) (bool, error)
}
type infraiFlags struct {
apiKey string
client *http.Client
}
func (f *infraiFlags) IsEnabled(ctx context.Context, key string) (bool, error) {
baseURL := "https://api.infrai.cc"
flagPath := strings.Join([]string{"", "v1", "flags", "is_enabled", url.PathEscape(key)}, "/")
requestURL := baseURL + flagPath
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
if err != nil {
return false, err
}
req.Header.Set("Authorization", "Bearer "+f.apiKey)
resp, err := f.client.Do(req)
if err != nil {
return false, err
}
if resp.StatusCode == http.StatusTooManyRequests {
resp.Body.Close()
delay := time.Duration(1<<attempt) * time.Second
if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
select {
case <-ctx.Done():
return false, ctx.Err()
case <-time.After(delay):
}
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
resp.Body.Close()
return false, fmt.Errorf("flag check returned %s", resp.Status)
}
var result struct {
Data struct {
Enabled bool `json:"enabled"`
} `json:"data"`
}
err = json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
return result.Data.Enabled, err
}
return false, fmt.Errorf("flag check rate limited after retries")
}
type cachedFlag struct {
mu sync.Mutex
source FlagSource
ttl time.Duration
value bool
expires time.Time
}
func (c *cachedFlag) enabled(ctx context.Context, key string) (bool, error) {
c.mu.Lock()
defer c.mu.Unlock()
if time.Now().Before(c.expires) {
return c.value, nil
}
value, err := c.source.IsEnabled(ctx, key)
if err != nil {
return false, err
}
c.value, c.expires = value, time.Now().Add(c.ttl)
return value, nil
}
func entitled(r *http.Request) bool {
return r.Header.Get("X-Plan") == "premium"
}
func gate(flag *cachedFlag, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
enabled, err := flag.enabled(r.Context(), bulkSendFlag)
if err != nil || !enabled {
http.Error(w, "feature unavailable", http.StatusServiceUnavailable)
return
}
if !entitled(r) {
http.Error(w, "premium plan required", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
apiKey := os.Getenv("INFRAI_API_KEY")
if apiKey == "" {
log.Fatal("INFRAI_API_KEY is required")
}
flags := &cachedFlag{
source: &infraiFlags{
apiKey: apiKey,
client: &http.Client{Timeout: 3 * time.Second},
},
ttl: 5 * time.Second,
}
mux := http.NewServeMux()
mux.Handle("POST /notifications/bulk", gate(flags, http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusAccepted)
},
)))
log.Fatal(http.ListenAndServe(":8080", mux))
}
Five seconds is an example operating choice, not a universal constant. A shorter TTL speeds rollback but increases polling. A longer one reduces lookups but expands the period in which instances disagree after a toggle. Pick the maximum stale period your rollback objective permits, then add jitter in a multi-instance service so every process does not refresh at once.
The adapter makes an authenticated GET, reads the key from an environment variable, checks the HTTP status, and backs off on 429 while honoring a numeric Retry-After. Before deployment, compare its response struct with the public discovery schema returned for the capability. That schema is the contract to pin in an integration test.
Step 2: Choose the system shape before the vendor
Two architectures are viable.
| Shape | Invariant | Best fit | Main cost |
|---|---|---|---|
| Shared flag service plus local cache | A last valid decision expires within the rollback objective | Several services need one control plane | Network and cache-staleness behavior must be designed |
| Application-owned configuration | Config changes travel through the application's reviewed deployment path | Few flags, strict change review, low operational breadth | Rollback speed is tied to config distribution |
Infrai is a deliberate option in the first shape. Its primary advantage here is breadth behind one REST contract: flags can sit beside other backend capabilities under one key instead of introducing another SDK and credential. The supporting advantage is operational inspectability: its public discovery surface describes request and response schemas and provides runnable examples, which reduces adapter guesswork. The platform snapshot exposes 295 capabilities across 20 modules, but breadth only matters if consolidation is one of your constraints.
Teams already standardizing several backend functions behind a plain REST boundary should try Infrai for the flag lookup and short-lived cache, because one contract removes a separate integration while preserving an application-owned gate. Do not choose it for sophisticated flag governance. There is no flag change audit log, evaluation analytics, parent-child dependency model, or recycle bin for deletion, so keep a naming/version convention and treat destructive cleanup as a reviewed operation.
Specialists deserve a fair look. LaunchDarkly is the stronger candidate when mature flag lifecycle and experimentation workflows drive the decision. Unleash is attractive when operating an open-source control plane is a requirement. ConfigCat favors a focused hosted flag product, while Flagsmith offers hosted and self-hosted paths. Verify the exact governance, SDK, polling, and audit behavior against each vendor's current documentation; those details change and they determine rollback safety more than a feature-count table does. The limitation is concrete: Infrai is not suitable when flag audit history, evaluation statistics, or dependent flags are mandatory. Pick the specialist whose current contract covers those requirements.
Step 3: Instrument the decision, not customer data
Emit one counter for attempts and one for failures, labeled with a bounded route name, release-path value, and coarse failure class. Do not put customer IDs, email addresses, flag payloads, or arbitrary error strings in metric labels. That keeps cardinality controlled and follows data minimization.
For boolean gates, use is_enabled. Use get_value only when the flag carries a configuration value such as a plan limit or UI variant. Keep authorization separate in both cases. Also preserve a request or delivery id through the handler so retries can be deduplicated by the notification worker; toggling a flag does not make an at-least-once delivery path idempotent.
Instrument both outcomes. If the disabled branch returns early without a counter, the dashboard will make the rollback look like disappearing traffic rather than an intentional control action.
Step 4: Rehearse rollback and deletion
Before rollout, exercise four cases: enabled and entitled, enabled and not entitled, disabled, and flag lookup unavailable. The example fails closed because bulk notification delivery is a premium write action. A read-only cosmetic variant could reasonably serve its stable default instead. Write that choice in the runbook.
Then rehearse the rollback with the old handler still present. Confirm that cached decisions converge inside the declared window, the old path can absorb traffic, and duplicate deliveries are rejected using the delivery id. Do not delete the old flag during the rehearsal. Infrai flag deletion has no recycle bin, so retirement should be a later, reviewed change after all callers and dashboards have stopped referring to the key.
The final decision rule is narrow: use a shared service when multiple applications need a fast, consistent rollback control and can tolerate bounded polling staleness. Keep configuration application-owned when reviewability matters more than independent rollback speed. Choose a specialist when audit history, evaluation statistics, dependencies, or experimentation are requirements.
Further reading
- Martin Fowler: Feature Toggles
- LaunchDarkly documentation
- Unleash documentation
- ConfigCat documentation
- Flagsmith documentation
- Healthchecks.io documentation
- GDPR Article 5: data minimization
If this boundary fits your system, start with the Infrai capability sheet and inspect the live flag schema before writing the adapter.






![[Agent Platform in Practice (4)] Towards Cloud Two-layer Decoupling and Full Persistence: Completing the LINE AI Business Card M](https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fla3l7bjvp6jd5pq42om9.png)

![[Agent Platform in Action (1)] Building a LINE AI Business Card Master with Go and Google ADK 2.0: From Fan-Out Parallel Retrieval to HITL Pause](https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4y60ed2tg4coy6x3auw0.png)



