Flags, rollouts and experiments
Runtime configuration delivered in one payload — feature flags, percentage rollouts, experiment variants, and remote values.
Four related capabilities, all served by the same runtime config payload the SDK fetches at startup.
| Namespace | Question | Returns |
|---|---|---|
config | What is this value? | Your typed value, or the fallback |
flags | Is this on? | boolean |
rollouts | Is this on for this device? | boolean |
experiments | Which variant does this device get? | string | null |
They resolve locally and synchronously#
Every one of these reads cached config. No network call, no promise — so they are safe to call during render:
function Checkout() { if (ScaleBun.flags.isEnabled('new_checkout')) return <NewCheckout />; return <LegacyCheckout />;}Defaults before config arrives#
On a first launch there is no cached config yet. Every accessor returns a conservative default rather than throwing or blocking:
| Call | Before config |
|---|---|
flags.isEnabled(key) | false |
rollouts.isOn(key) | false |
experiments.variant(key) | null |
config.get(key, fallback) | your fallback |
Remote values#
For anything that is not a boolean:
const pageSize = ScaleBun.config.get('list_page_size', 20);const banner = ScaleBun.config.get('promo_banner_text', '');const limits = ScaleBun.config.get('upload_limits', { maxMb: 10 });The fallback is required, and it is also the type source — pass the value you would ship if the config service never answered.
Force a refresh when you need one (after sign-in changes segment membership, for example):
await ScaleBun.config.refresh();Rollouts vs flags#
A flag is a global switch. A rollout is a flag with a percentage: the same
device consistently gets the same answer, so a user does not see a feature appear
and disappear between launches. That consistency comes from bucketing on the device
id, which is why clearUser() does not change a rollout assignment — identity and
bucketing are separate.
if (ScaleBun.rollouts.isOn('beta_paywall')) { // Stable for this device across launches.}Experiments#
const variant = ScaleBun.experiments.variant('checkout_cta');
switch (variant) { case 'treatment': return <CtaB />; case 'control': return <CtaA />; default: // null = not enrolled, or config has not arrived. Show the control. return <CtaA />;}Handle null explicitly. Treating it as "treatment" enrols unassigned devices into
your test arm and quietly invalidates the result.
To analyse an experiment you need the variant on the events you care about, so read it once and attach it:
ScaleBun.track('checkout_completed', { variant: ScaleBun.experiments.variant('checkout_cta') ?? 'unassigned',});Entitlements ride the same payload#
The config response also carries entitlements — which capabilities your plan allows. The SDK applies them before registering a collector, so an unentitled capability produces no traffic at all. See Architecture.
Related#
Events — attaching variants for analysis.