@platform/sdk-flags
@platform/sdk-flags provides feature flags, A/B test variants, and
canary release controls for module authors. Flag evaluation is an
in-memory lookup — no network call happens at eval time.
The SDK polls the GoFeatureFlag service every 15 seconds in the
background.
Installation
pnpm add @platform/sdk-flags
isEnabled()
Check whether a feature flag is enabled for the current user and tenant:
import { kernel } from '@platform/sdk-core';
// Simple boolean gate
if (kernel.flags().isEnabled('new-checkout-flow')) {
return <NewCheckout />;
}
// Usage in React hook form
const showBetaEditor = kernel.flags().isEnabled('beta-editor');
No network call.
isEnabled()evaluates against the in-memory cache — response time is in nanoseconds. The background poller refreshes the cache every 15 seconds without blocking your code.
Evaluation Context
The SDK automatically injects evaluation context from the current session — module authors never pass user/tenant data manually:
| Context Field | Source | Used for |
|---|---|---|
userId | JWT | Per-user targeting and percentage rollout |
tenantId | JWT | Tenant-level flag isolation |
roles | JWT | Role-based flag targeting |
environment | kernel.init() config | Environment-specific flags (staging, production) |
percentage | Computed | Canary rollout — consistent hashing ensures user always gets same bucket |
Tenant Isolation
The SDK automatically prefixes every flag name with {tenantId}:.
Module authors use short names — namespacing is transparent:
kernel.flags().isEnabled('new-checkout-flow')
// SDK resolves: isEnabled('01j9p-tenant:new-checkout-flow')
// Tenant A and Tenant B can have different states for the same flag name
getVariant()
For A/B tests and multivariate experiments, get the variant assigned to the current user:
const variant = kernel.flags().getVariant('checkout-button-color');
// Returns: 'control' | 'blue' | 'green' | 'orange' (configured in Admin UI)
// Returns: 'control' if flag is disabled or user not in experiment
switch (variant) {
case 'blue': return <Button color="#1a73e8">Buy now</Button>;
case 'green': return <Button color="#34a853">Buy now</Button>;
case 'orange': return <Button color="#fa7b17">Buy now</Button>;
default: return <Button>Buy now</Button>;
}
Variants are configured in the Admin UI (Settings → Feature Flags). Each flag supports up to 10 variants. Max payload per variant: 4 KB JSON.
snapshot()
Capture an immutable snapshot of all current flag states for a session. Use this for critical user flows (checkout, form multi-step) where a flag change mid-session would break the UX:
class CheckoutSession {
private flags = kernel.flags().snapshot();
// During this session, flag values are frozen even if the 15s poll
// delivers an update.
isExpressCheckoutEnabled() {
return this.flags.isEnabled('express-checkout');
}
getPaymentVariant() {
return this.flags.getVariant('payment-layout');
}
}
snapshot() returns a frozen copy. Subsequent 15-second polls do not
affect values inside a snapshot. Create a new snapshot to get fresh values.
Without
snapshot():getVariant()returns the current cached value — it may change between calls if the 15-second poll fires mid-session. For most UI components this is fine. For transactional flows (cart, payments) always usesnapshot().
listFlags()
List all flags registered for the current tenant:
const flags = await kernel.flags().listFlags();
// [
// { key: 'new-checkout-flow', enabled: true, rollout: 50, variants: [], stale: false },
// { key: 'beta-editor', enabled: false, rollout: 0, variants: [], stale: false },
// { key: 'old-header', enabled: true, rollout: 100, variants: [], stale: true },
// ]
Caching and Fallback
| State | Behavior |
|---|---|
| Normal operation | In-memory cache, updated every 15 seconds |
| GoFeatureFlag service down | SDK returns last cached values — works autonomously |
| Cold start (empty cache) + service down | All flags return false (safe default) |
| Cold start + Valkey available | Pod fetches stale cache from Valkey → serves traffic. Pod = NOT READY until at least a stale cache is loaded |
| Pod with empty cache | Kubernetes readiness probe = NOT READY — no traffic is routed to it |
Readiness guarantee: A pod with a completely empty flag cache reports
NOT READYto Kubernetes. This prevents the flag-less pod from serving requests where a new feature would always appear disabled. Existing warm-cache pods continue serving traffic uninterrupted.
Optimistic Locking (Admin — Conflict Resolution)
Two admins editing the same flag simultaneously is handled by
optimistic locking on the version field:
PATCH https://api.septemcore.com/v1/flags/new-checkout-flow
Authorization: Bearer <access_token>
Content-Type: application/json
{ "enabled": true, "rollout": 75, "version": 4 }
If another admin has already updated the flag (version is now 5):
{
"type": "https://api.septemcore.com/problems/flag-version-conflict",
"status": 409,
"title": "Flag version conflict",
"detail": "Flag was modified by [email protected] at 2026-04-22T01:30:00Z. Current value: rollout=100. Your value: rollout=75."
}
Every flag mutation is logged: flags.updated → Audit Service
with { before, after, version }.
Stale Flag Cleanup
| Parameter | Value |
|---|---|
| Stale detection threshold | Flag unchanged for 90 days AND evaluates identically for 100% of requests |
| Admin notification | Admin UI badge: "X flags may be stale. Archive?" |
| Archived flag behavior | Returns false (safe default). Referencing archived flag in code = no crash |
| Archived flag retention | 180 days in PostgreSQL, then physical delete |
| Billing limit enforcement | 402 Payment Required when tenant exceeds plan flag limit. FLAGS_MAX_PER_TENANT=0 = unlimited (safety net env). Stale cleanup keeps active count under limit. |
REST API Reference
For layout brevity, the /api/v1 base path prefix is omitted
from the endpoint table below.
| Method | Endpoint | Description |
|---|---|---|
GET | /flags | List all flags (paginated) |
GET | /flags/:key | Get flag state and config |
POST | /flags | Create flag |
PATCH | /flags/:key | Update flag (requires version) |
DELETE | /flags/:key | Archive flag (soft delete) |