Skip to main content

@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 FieldSourceUsed for
userIdJWTPer-user targeting and percentage rollout
tenantIdJWTTenant-level flag isolation
rolesJWTRole-based flag targeting
environmentkernel.init() configEnvironment-specific flags (staging, production)
percentageComputedCanary 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 use snapshot().


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

StateBehavior
Normal operationIn-memory cache, updated every 15 seconds
GoFeatureFlag service downSDK returns last cached values — works autonomously
Cold start (empty cache) + service downAll flags return false (safe default)
Cold start + Valkey availablePod fetches stale cache from Valkey → serves traffic. Pod = NOT READY until at least a stale cache is loaded
Pod with empty cacheKubernetes readiness probe = NOT READY — no traffic is routed to it

Readiness guarantee: A pod with a completely empty flag cache reports NOT READY to 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

ParameterValue
Stale detection thresholdFlag unchanged for 90 days AND evaluates identically for 100% of requests
Admin notificationAdmin UI badge: "X flags may be stale. Archive?"
Archived flag behaviorReturns false (safe default). Referencing archived flag in code = no crash
Archived flag retention180 days in PostgreSQL, then physical delete
Billing limit enforcement402 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

Base Path

For layout brevity, the /api/v1 base path prefix is omitted from the endpoint table below.

MethodEndpointDescription
GET/flagsList all flags (paginated)
GET/flags/:keyGet flag state and config
POST/flagsCreate flag
PATCH/flags/:keyUpdate flag (requires version)
DELETE/flags/:keyArchive flag (soft delete)