Choose a versioned snapshot for server-side feature flag reads, and reserve live API calls for authenticated admin writes. For a B2B SaaS agent loop, that split keeps rendering independent of the control plane while preserving the dimensions needed to attribute model cost to a tenant, flag revision, and agent run. A live lookup on every render is simpler on a whiteboard; it's the wrong default once a toggle can change both latency and spend.
The boundary is narrow. If flags change only during deploys, configuration in the deployment artifact is cheaper to operate than an admin page. If a toggle must react within milliseconds or coordinate a transaction, a cached snapshot is also the wrong primitive; use a system designed for coordinated state. For ordinary operational changes where a short propagation interval is acceptable, snapshots give the on-call engineer a smaller failure domain and a rollback that can be rehearsed.
What should a Next.js feature flag admin page send to a backend API?
The page should send intent, not an entire mutable configuration document. A useful write contains the flag name, desired state, expected revision, reason, and actor established by server-side authentication. The backend owns authorization, revision checks, validation, persistence, audit history, and publication of the next snapshot. The Next.js page can be server-rendered from the current administrative state, but the browser must never become the authority for tenant scope or actor identity.
Use compare-and-set semantics. If two operators opened revision 41 and the first saved revision 42, the second write should receive 409 Conflict instead of silently replacing the newer decision. Reject a malformed reason or unknown flag with 422 Unprocessable Entity; reject an unauthenticated request with 401 Unauthorized. Those codes are an interface policy, so test them as contract behavior rather than leaving them to whichever storage error happens to surface.
This split matters in an AI agent loop because enabled is rarely enough for cost attribution. The evaluation record should carry an immutable flag revision and a variant such as baseline or candidate. The usage record should carry the same revision beside tenant ID, agent run ID, operation name, model identifier, input units, output units, duration, and outcome. Don't put raw prompts into flag labels or metric dimensions. They create uncontrolled cardinality and complicate erasure work.
A compact control-plane shape can look like this:
type UpdateFlag struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
ExpectedRevision uint64 `json:"expected_revision"`
Reason string `json:"reason"`
}
type FlagSnapshot struct {
Revision uint64 `json:"revision"`
PublishedAt time.Time `json:"published_at"`
Flags map[string]bool `json:"flags"`
}
type Evaluation struct {
TenantID string
AgentRunID string
FlagName string
FlagEnabled bool
Revision uint64
}
Keep the reason in the audit record, not in every request metric. Keep actor identity there too. If a tenant invokes a right-to-erasure workflow, the data inventory must make clear which audit and usage records contain personal data; GDPR Article 17 describes the erasure right and its exceptions, so retention cannot be decided by an observability dashboard alone.
The failure mode is split-brain attribution
A toggle system can appear healthy while its accounting is false. Imagine revision 57 enables an extra retrieval step. One server refreshes immediately, another still holds revision 56, and both report usage under a label that says only retrieval=true. The chart looks tidy, yet no one can prove which policy each request evaluated. During an incident, the team may disable the step and see aggregate spend fall while a stale instance continues to run it. The operational problem is not merely stale configuration; it is evidence that cannot be joined.
Revision-first telemetry fixes the join. Emit the decision at evaluation time, then pass that decision through the agent context rather than reevaluating midway through the loop. Every model call derived from that decision inherits the same tenant ID, agent run ID, flag name, variant, and revision. This also gives event grouping a deliberate boundary: Sentry documents that grouping can be influenced through fingerprints, but a fingerprint should describe an actionable failure class, not a tenant or a unique run. Otherwise one underlying defect fragments into many groups.
Be strict here.
A capacity plan should treat snapshot refreshes and admin writes as control-plane load, while treating evaluations as data-plane load. If the rendering fleet grows tenfold, read traffic should remain local to each process or region instead of multiplying calls to the administrative backend. Set separate SLOs: the serving SLO covers correct local evaluation and request latency; the control-plane SLO covers accepted-write durability and propagation age. A single availability percentage hides which promise was broken.
I'm not sure what propagation target fits every product. The answer depends on how quickly an operator expects a cost guard to take effect and how much stale work the business can tolerate. Resolve that uncertainty with a budget stated in time and exposure: define a maximum snapshot age and a maximum number of agent runs allowed under the previous revision, then test both. Those are policy variables, not universal thresholds.
Safe snapshot implementation
The read path should be boring: load one immutable value, evaluate it, attach its revision to the request context, and continue. Publication builds a fresh map and swaps it atomically; it never mutates a map that concurrent requests can see. This Go sketch is the backend-side core that a Next.js server component can use through application logic, without putting a control-plane request in the render path.
type Store struct {
current atomic.Value // Stores FlagSnapshot.
}
func NewStore(initial FlagSnapshot) *Store {
s := &Store{}
s.current.Store(cloneSnapshot(initial))
return s
}
func (s *Store) Evaluate(tenantID, runID, name string) (Evaluation, bool) {
snapshot := s.current.Load().(FlagSnapshot)
enabled, known := snapshot.Flags[name]
if !known {
return Evaluation{}, false
}
return Evaluation{
TenantID: tenantID, AgentRunID: runID, FlagName: name,
FlagEnabled: enabled, Revision: snapshot.Revision,
}, true
}
func (s *Store) Publish(next FlagSnapshot) error {
current := s.current.Load().(FlagSnapshot)
if next.Revision != current.Revision+1 {
return fmt.Errorf("non-sequential revision: got %d after %d", next.Revision, current.Revision)
}
s.current.Store(cloneSnapshot(next))
return nil
}
func cloneSnapshot(in FlagSnapshot) FlagSnapshot {
flags := make(map[string]bool, len(in.Flags))
for name, enabled := range in.Flags {
flags[name] = enabled
}
in.Flags = flags
return in
}
Unknown flags should fail closed to a documented baseline, not silently become false in a way that looks like a deliberate evaluation. In this interface, known forces the caller to choose. For an agent capability that incurs cost, the conservative baseline may disable the optional step; for a safety control, the conservative baseline may enable it. There is no universal boolean default. Record the selected baseline in the flag definition and cover it in a test.
The buy-versus-build decision is mostly about ownership, not toggle syntax:
| Concern | Small internal control plane | Managed flag service |
|---|---|---|
| On-call surface | Your team owns auth, storage, publication, and audit recovery | Provider owns service operation; your team still owns integration behavior |
| Cost attribution | Schema can match the agent ledger exactly | Exported evaluation data must join cleanly to the ledger |
| Lock-in | Internal schema and snapshot format are yours | SDK and targeting semantics can become application dependencies |
| Best fit | Few flags, simple targeting, team can support the control plane | Many flags, complex targeting, governance needs exceed build appetite |
Build is not automatically cheap. On-call time, access reviews, audit restoration, and schema migration are capacity costs even when infrastructure spend is small. The catch is equally real on the managed side: if its evaluation model cannot preserve your agent run ID and immutable revision at the usage boundary, the richer feature set doesn't repair cost attribution. Choose based on the evidence you need during a rollback.
Verify before enabling writes
Start with shadow evaluation. Load snapshots and calculate decisions, but keep the existing behavior authoritative; compare decision counts by revision and flag without attaching tenant IDs to low-level metrics. Then enable the admin write path for a restricted operator group, publish a no-op revision, and verify that every serving instance reports the new revision within the propagation budget.
The deployment gate needs four checks:
- A stale expected revision returns
409, and the stored flag remains unchanged. - An unknown flag takes the declared baseline and emits a distinct evaluation outcome.
- Each agent usage row joins to exactly one evaluation by tenant, run, flag, and revision.
- A snapshot older than the agreed limit raises an alert before the serving SLO is exhausted.
Don't test rollback only by flipping the UI twice. Capture revision 58, publish revision 59, and roll back by publishing revision 60 whose values copy the known-good state from 58. Revisions remain monotonic, the audit trail remains intelligible, and in-flight runs retain the decision they began with. Reusing 58 as the current revision would make later usage ambiguous.
Short test. Long consequence.
Cost verification belongs in the same gate. Reconcile aggregate input and output units from the usage ledger against evaluation counts for each revision, and quarantine rows that lack a join rather than assigning them to the latest flag state. A dashboard may aggregate this data, but the ledger is the accountability boundary. If the join fails, stop the rollout; an attractive latency chart cannot compensate for unattributed spend.
Rollback and operating boundaries
Rollback means publishing a new snapshot with known-good values, confirming fleet convergence, and watching both agent latency and attributed usage until the rollback window closes. Preserve the original decision as audit history. For error tracking, group failures by stable code location or a deliberate low-cardinality fingerprint; Sentry's grouping guidance explains why fingerprints change grouping behavior, so adding tenant or run identifiers there would make operational triage harder.
Stick with deployment configuration when changes can wait for a release and there is no need for delegated operators. Choose live coordinated state when sub-second global convergence is a hard correctness requirement. Choose a managed service when targeting, approvals, experimentation, or multi-region publication would consume more platform capacity than the team can defend; choose a small internal control plane when rules are few, the audit model is narrow, and the team accepts the on-call ownership.
No option removes the need to define stale behavior.
The final go/no-go rule is evidence-based: ship the snapshot design only when every agent usage record can be attributed to one immutable flag revision, the fleet meets a measured propagation budget, and rollback has been exercised as a forward revision. Otherwise keep the feature behind deployment configuration while the control plane is made operable.
Top comments (0)