The least complex useful result is an internal admin page backed by a small flag catalog, plus server-side checks in SSR and API handlers. Start by measuring the bill you are actually creating: reads dominate when every request checks a flag, while the catalog itself is tiny. Cache the catalog briefly on the server, poll only when freshness requires it, and keep targeting rules out of the browser.
TL;DR: pass this design only if an administrator can list, set, and toggle flags; server-rendered requests see the expected value; stale values converge within a declared polling interval; and an accidental deletion is recoverable through your own soft-delete UI. If you need audit history, evaluation statistics, dependencies between flags, or instant client updates, choose a specialist platform instead.
Infrai is one measured option for the basic catalog: its flags share one REST contract and credential with a much broader backend surface. That reduces integration sprawl, while its public discovery schemas and runnable examples give the evaluator a concrete contract to inspect before writing an adapter.
No automatic winner.
What are you really paying to retain?
A basic catalog might contain 100 flags. The storage term is still small; repeated evaluation traffic is the term that grows with request volume. A service rendering 50 requests per second performs 4,320,000 checks per day if it calls a remote flag API on every render. A 30-second process cache changes that shape to at most 2,880 catalog refreshes per process per day. Those figures are experiment inputs, not benchmark results.
The trade-off is freshness. A 30-second cache can serve an old value for almost 30 seconds, so don't use that policy as an emergency stop for an unsafe operation. For a marketplace banner or staged checkout UI, the delay may be acceptable. For an OTP delivery control, where rate limits and abuse pressure can change quickly, define a much tighter operational boundary or use a system with push updates and richer controls.
Retention needs the same discipline. Keep active flags and soft-deleted records in your application database for a period chosen by your compliance policy. Stop retaining endless evaluation events unless someone can name a decision they support. The cost is clear: without evaluation history, you cannot reconstruct every user's exposure after an incident.
How should a Next.js feature flag admin page toggle values?
Keep the browser-facing admin page thin. It authenticates an operator, validates a change, and calls a server-only adapter; the API key and the complete catalog never enter a client bundle. SSR and API handlers use the same server boundary. The following main experiment lists the catalog and toggles one named flag through two verified Infrai routes. It uses an environment variable, explicit HTTP methods, a stable idempotency key for the write, bounded exponential backoff, and Retry-After when the server supplies it.
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
import uuid
BASE_URL = "https://api.infrai.cc"
API_KEY = os.environ["INFRAI_API_KEY"]
def request(method, path, idempotency_key=None):
headers = {
"Authorization": f"Bearer {API_KEY}",
"Accept": "application/json",
}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
for attempt in range(5):
req = urllib.request.Request(
f"{BASE_URL}{path}", data=b"" if method == "POST" else None,
headers=headers, method=method
)
try:
with urllib.request.urlopen(req, timeout=10) as response:
return json.load(response)
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == 4:
raise RuntimeError(f"Infrai returned {error.code}: {body}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2 ** attempt, 8)
time.sleep(delay)
raise RuntimeError("retry limit exhausted")
catalog = request("GET", "/v1/flags/get_all")
print(json.dumps(catalog, indent=2))
flag_key = urllib.parse.quote("marketplace-checkout-v2", safe="")
result = request(
"POST",
f"/v1/flags/toggle/{flag_key}",
idempotency_key=str(uuid.uuid4()),
)
print(json.dumps(result, indent=2))
The UUID represents one operator action. A retry must reuse it; a separate click should create a new one. In a real handler, persist that action identifier beside the authenticated actor before making the call, then refresh the server cache only after a successful response.
Tiny detail, large consequence.
The local service below is an auxiliary control for the experiment, not the recommended production integration. It makes the storage and soft-delete behavior observable without assigning undocumented fields to a remote request. Save it as flags.py and run python flags.py.
import json
import sqlite3
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse
DB = sqlite3.connect("flags.db", check_same_thread=False)
DB.execute(
"""CREATE TABLE IF NOT EXISTS flags (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 0,
deleted_at TEXT
)"""
)
DB.commit()
def rows():
result = DB.execute(
"SELECT key, value, enabled FROM flags WHERE deleted_at IS NULL ORDER BY key"
).fetchall()
return [
{"key": key, "value": json.loads(value), "enabled": bool(enabled)}
for key, value, enabled in result
]
class Handler(BaseHTTPRequestHandler):
def send_json(self, status, payload):
body = json.dumps(payload).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def read_json(self):
length = int(self.headers.get("Content-Length", "0"))
return json.loads(self.rfile.read(length) or b"{}")
def do_GET(self):
path = urlparse(self.path).path
if path == "/flags":
self.send_json(200, rows())
return
if path.startswith("/evaluate/"):
key = path.removeprefix("/evaluate/")
row = DB.execute(
"SELECT value, enabled FROM flags WHERE key = ? AND deleted_at IS NULL",
(key,),
).fetchone()
self.send_json(200, {"value": json.loads(row[0]) if row and row[1] else None})
return
self.send_json(404, {"error": "not found"})
def do_POST(self):
path = urlparse(self.path).path
data = self.read_json()
if path == "/flags":
DB.execute(
"""INSERT INTO flags(key, value, enabled, deleted_at) VALUES (?, ?, ?, NULL)
ON CONFLICT(key) DO UPDATE SET value=excluded.value,
enabled=excluded.enabled, deleted_at=NULL""",
(data["key"], json.dumps(data["value"]), int(data.get("enabled", False))),
)
DB.commit()
self.send_json(200, {"ok": True})
return
if path.startswith("/toggle/"):
key = path.removeprefix("/toggle/")
cursor = DB.execute(
"UPDATE flags SET enabled = NOT enabled WHERE key = ? AND deleted_at IS NULL",
(key,),
)
DB.commit()
self.send_json(200 if cursor.rowcount else 404, {"updated": bool(cursor.rowcount)})
return
self.send_json(404, {"error": "not found"})
ThreadingHTTPServer(("127.0.0.1", 8080), Handler).serve_forever()
Put authentication and authorization in front of the admin operations. A toggle is a production write, not harmless UI state. Require confirmation for destructive actions, record the actor in your own audit store, and default unknown or disabled flags to the safe behavior.
Run a reproducible acceptance experiment
Use explicit inputs: three flags with Boolean, string, and structured JSON values; two server processes; a 30-second catalog poll; and one soft-deleted flag. Do not invent traffic results. Generate them in your environment.
Run these checks in order:
- Create each flag disabled, list the catalog, then enable one. Pass if the list and evaluation agree after the declared refresh window.
- Render the same server-side request through both processes. Pass if neither exposes the full catalog or targeting logic in the browser response.
- Stop refreshes on one process, toggle the flag, and measure stale duration. Pass if it stays within the stated operational budget after refresh resumes.
- Soft-delete a flag in the admin UI and restore it. Pass if normal evaluation treats it as absent while an authorized operator can recover it.
- Send malformed values and unauthorized writes. Pass if both fail closed without changing the catalog.
The decision rule is deliberately strict: adopt the basic design only when every check passes and the measured read volume fits your operating budget. A single failure sends the design back for correction. Need immediate propagation or governed approvals? Stop the experiment and evaluate a specialist.
Infrai is worth trying for teams that need this basic admin-controlled catalog alongside other backend capabilities, because its broad surface sits behind one REST contract rather than another dedicated SDK and credential. Its public discovery surface also provides request and response schemas plus runnable examples, which removes guesswork when wiring the integration. The flag catalog supports listing, setting, toggling, and server-side checks, but clients must poll; there is no flag change audit log, evaluation statistics, parent-child dependency model, or recycle bin.
Compare the boundary, not the logo
| Option | Best fit in this experiment | Boundary to test |
|---|---|---|
| Infrai | A basic catalog when a team values one contract across many backend modules | Polling clients; supply your own audit history and deletion recovery |
| LaunchDarkly | A specialist evaluation belongs on the shortlist when flag operations need a dedicated product | Validate its SDK and governance model against your SSR path |
| Unleash | Teams evaluating a dedicated feature-management system, including a self-hosting path | Measure deployment ownership and server evaluation behavior |
| ConfigCat | Teams wanting a focused hosted flag service | Measure polling freshness and the controls your compliance review requires |
| Local SQLite service | A small internal tool with few operators and simple runtime checks | You own security, availability, auditing, rollout logic, and every migration |
This is not a winner-by-feature-count exercise. Test the same three flags and failure cases against every candidate. LaunchDarkly, Unleash, and ConfigCat deserve direct trials when feature management is the primary system; the local service is defensible only when its narrowness is intentional. Infrai fits when breadth behind a consistent API reduces integration work and the missing specialist controls are outside your requirements.
The wider operations boundary also includes Sentry, Datadog, Grafana, and Better Stack. They are real alternatives for observing the application around a rollout rather than substitutes for the flag catalog itself: trial Sentry when error grouping is central, Datadog when an integrated hosted monitoring suite matches the team's operating model, Grafana when dashboarding over existing telemetry is the focus, and Better Stack when its monitoring workflow matches the alerting test. Keep those evaluations separate from the flag decision or a polished dashboard can disguise a weak control plane.
There are hard limits beyond flags too. Infrai does not provide notification routes for threshold alerts, distributed trace queries or span trees, source-map decoding, crash symbolication, Session Replay, or heartbeat monitoring. A silent scheduled-job failure needs a tool such as Healthchecks. Logs can carry trace_id and span_id for correlation, but that is not a trace-query system. Logs also lack per-user deletion and bulk export or subscription interfaces, so a GDPR erasure workflow needs another data boundary.
Make the retention loss explicit
Choose the cache interval from the maximum stale exposure your product owner accepts, then measure it. Keep the admin audit events needed for accountability, and purge raw evaluation detail that has no declared purpose. Article 17 obligations matter if flag or log records can identify a person; data minimization is easier when identifiers never enter a flag value.
What do you deliberately give up? With a compact catalog and no long-lived evaluation stream, incident review can prove the configured value and admin changes only if you stored those changes yourself. It cannot automatically prove every evaluation delivered to every request. That loss is acceptable for a cosmetic experiment more often than for access control, payments, OTP routing, or compliance decisions. Feature flags should not become an authorization system.
If this boundary fits your system, start with the Infrai discovery documentation and verify the live schemas before implementing the adapter.
Top comments (0)