Experimentation

Remote Config

Typed config values with cohort-targeted overrides, version history, A/B item experiments, change-impact analysis, and cohort-adoption metrics. The values you'd otherwise hard-code, ready to ship without a deploy.

Remote Config (engine module: configmod) is Sankofa's typed-value-shipping product. It's what you reach for when:

  • You'd otherwise hard-code a value (timeout, threshold, max-upload-MB) and want to change it without a deploy.
  • You want to A/B test a value (price, copy, retry count) — not a code path.
  • You want to ship copy, JSON payloads, or pricing changes faster than your release cadence.
  • You want a cohort-specific config without forking your code.

It shares the decision handshake with Switch + Pulse, so reads are local and free after the first fetch.

What's in Remote Config

ConceptPurpose
ItemA named typed value. The unit your code reads via config.get<T>(key, default).
TypeOne of string, int, float, bool, json. Immutable after creation.
Default valueThe current value returned if no override matches.
RuleThe targeting logic for an item — cohort + property predicates.
OverrideA cohort-scoped value that wins over the default for matching users.
VersionEvery change creates a new immutable version.
A/B experimentTwo values for a single item, randomly assigned per user, with significance metrics.
Change impactEstimated rollout impact (users / cohorts affected) before publish.
Cohort adoptionPer-cohort timeline of which version is live.

Typed values

Every item is one of five types, declared at creation:

TypeDefault value exampleSDK getter (web example)
string"[email protected]"config.get<string>("support_email", "[email protected]")
int25config.get<number>("max_upload_mb", 25)
float0.05config.get<number>("conversion_threshold", 0.05)
booltrueconfig.get<boolean>("show_promotion_banner", false)
json{...}config.get<HomeHero>("home_hero", DEFAULT)

The type is immutable after creation. To "change the type" of a key, archive the old key and create a new one with a different name + type.

Rules and cohort-targeted overrides

A rule lets you ship one value to most users and a different value to a cohort:

JSON
{
"key": "max_upload_mb",
"type": "int",
"default_value": 25,
"overrides": [
  {
    "cohort": "Pro plan customers",
    "value": 200
  },
  {
    "cohort": "Internal staff",
    "value": 1000
  }
]
}

The rule is evaluated server-side at handshake time; the SDK only sees the final value for the active user.

Version history + rollback

Every publish creates a new immutable version. The dashboard's version log shows:

  • What changed — diff between consecutive versions
  • Who changed it — actor (with project-role attribution)
  • When — UTC timestamp
  • Why — optional change note

Rollback is non-destructive — it creates a new version whose snapshot matches a previous version. So the version timeline is append-only, even when you "undo".

bash
sankofa config rollback max_upload_mb 3 --note "revert accidental bump"

A/B experiments on items

Config supports the same variant testing as Switch, but for values:

JSON
{
"key": "checkout_button_label",
"type": "string",
"experiment": {
  "variants": [
    { "name": "control", "value": "Pay now", "weight": 50 },
    { "name": "treatment", "value": "Confirm purchase", "weight": 50 }
  ],
  "success_metrics": ["checkout_completed"]
}
}

Variant assignment uses the same stable hashing as Switch. The experiment dashboard shows lift + statistical significance computed against the success-metric events you defined.

A common pattern: A/B test the value of a config item that drives an experience that's already controlled by a Switch flag. Switch decides whether the user sees the experience at all; Config decides which variant of the value they see.

Change impact + cohort adoption

Before you publish a rule change, the dashboard previews:

  • Estimated reach — total users + per-cohort breakdown affected by the new rule.
  • Risk score — derived from the % of high-value users (configurable) the change affects.
  • Cohort adoption timeline — for an existing item, how each cohort's value has changed over time.

This sits at /dashboard/config/items/:id/impact and is computed against ClickHouse so the numbers are fresh to within minutes.

Subscriptions

Apps that need to react to mid-session value changes (admin published a new override and the UI should refresh) subscribe to the change stream:

TypeScript
config.onChange("max_upload_mb", (decision) => {
setMaxMB(decision.value);
});

Subscriptions fire on the next handshake refresh that returns a different value (typically within 30 s of dashboard publish).

Bundled defaults

Every SDK lets you ship bundled defaults at init time. Defaults serve three purposes:

  1. Synchronous startup — your code calls get<T>(...) immediately after init, before the engine has responded. The bundled default returns synchronously.
  2. Offline fallback — fresh installs with no cached snapshot return defaults until connectivity returns.
  3. Type contract — establishes the type the SDK expects. Engine type mismatches preserve the default.

API surface

EndpointPurpose
GET /api/v1/config/itemsList items.
POST /api/v1/config/itemsCreate an item.
GET /api/v1/config/items/:idRead item + current version.
PUT /api/v1/config/items/:id/ruleUpdate the targeting rule.
GET /api/v1/config/items/:id/versionsList versions.
POST /api/v1/config/items/:id/rollbackRoll back to a previous version.
GET /api/v1/config/items/:id/change-impactPre-flight estimate of a rule change.
GET /api/v1/config/items/:id/cohort-adoptionPer-cohort adoption timeline.
GET /api/v1/config/metricsAggregated metrics across the project.
GET /api/v1/handshakeUnified decision-handshake endpoint (called by the SDK).

Config limits by tier

PlanItemsFetches / monthVersionsCohort overridesA/B experimentsHistory retention
Hobby≤ 25100K5basic
Pro10Munlimitedup to 5 active30 days
Growthunlimitedup to 25 active90 days
Enterpriseunlimitedunlimitedcustom

What's next

Edit this page on GitHub