MIRA FIVE

Feature flags

Read feature flags, variants and remote config in the browser and on a server, target them, and render them without flicker.

A feature flag decides, per visitor or per signed-in person, whether a feature is on, which variant they see, or which configuration value applies. You define flags in MIRA FIVE; the SDKs fetch them as a flag document and evaluate every read locally, so a read is synchronous and never waits for the network. For the endpoints themselves, see Feature flags in the ingest API.

Flag documents

MIRA FIVE compiles the live flags of each source into a document. Which one an SDK receives depends on the source and the mode:

DocumentServed toContent
BrowserBrowser SDKs in full mode on a full website sourceThe rules of every flag the website reads
ValuesBrowser SDKs in consentless mode, and every consentless website sourceThe answer each flag gives someone the SDK knows nothing about
ServerServer SDKs, with the secret keyThe rules of every flag servers read

A flag only servers read never reaches a browser. Documents carry no flag names or descriptions, and a segment appears only as an opaque reference.

Flag types

TypeBrowser flag() returnsServer read
On/offtrue or falseenabled(key) returns true or false
VariantsThe variant key, for example 'b'variant(key, fallback) returns the variant key
Remote configThe variant keyconfig(key, fallback) returns the variant's JSON value

Any variant can carry a JSON value; config(key, fallback) returns the value of the variant the visitor gets, in the browser and on a server.

Each flag is assigned by one unit: by browser, using the anonymous id, or by person, using the user id you pass to identify() or to a server read. The same unit always lands in the same variant.

Read flags in the browser

Add flags to the client, then read them once they have loaded:

<script>window.mirafive=window.mirafive||function(){(mirafive.q=mirafive.q||[]).push(arguments)}</script>
<script defer src="https://cdn.mirafive.io/mira.js" data-key="mf_…" data-flags></script>
<script>
  mirafive('flags', () => {
    const newCheckout = mirafive('flag', 'new-checkout', false)
    const limits = mirafive('config', 'checkout-limits', { maxItems: 10 })

    document.body.classList.toggle('new-checkout', newCheckout === true)
    document.body.dataset.maxItems = String(limits.maxItems)
  })
</script>

data-flags loads flags at once; without it they load on the first flag verb. Until then, flag and config return the fallback, so read them inside the flags listener. It runs once flags have loaded and again when they change.

Read flags on a server

Server SDKs fetch the server document with the secret key. Ask for one unit's flags, then read them:

import { Mira } from '@mirafive/sdk-server'
import { MiraFlags } from '@mirafive/sdk-server/flags'

const mira = new Mira({ key: process.env.MIRAFIVE_SECRET_KEY })
const flags = new MiraFlags({ key: process.env.MIRAFIVE_SECRET_KEY, mira })

const user = await flags.for({
  userId: session.userId,
  anonymousId: request.headers.get('X-Anonymous-Id') ?? undefined,
  properties: { plan: session.plan },
  consent: { experiments: true, targeting: false },
  optedOut: request.headers.get('Sec-GPC') === '1' || request.headers.get('DNT') === '1',
})

const newCheckout = user.enabled('new-checkout')
const pricing = user.variant('pricing-test', 'control')
const limits = user.config('checkout-limits', { maxItems: 10 })

Keep one MiraFlags per process. The first for() waits up to 1.5 seconds (timeoutMs) for the document; later reads use the one in memory.

The unit:

FieldDescription
userIdYour id for the signed-in person. The unit of flags assigned by person
anonymousIdThe browser's anonymous id, from anonymousId() in the page. The unit of flags assigned by browser. Anything after a . is ignored
propertiesFacts your targeting rules test. Held in memory, never sent
consentThe visitor's answer for experiments and targeting. A scope you leave out counts as granted: your own lawful basis applies
optedOuttrue for a request with Sec-GPC: 1 or DNT: 1: no ids, no segment lookup, no exposure

Frameworks read the opt-out for you: flagsFor() in Next.js and TanStack Start, miraFlagsFor() in Nuxt and Astro, and Laravel.

Fallbacks

Every read takes a fallback, and returns it when there is no answer:

  • flags have not loaded yet (in the browser), or no document has arrived (on a server);
  • the flag is not served to this source, or does not exist. In development, the browser SDK warns: [mirafive] flag "new-checkout" is unknown;
  • in the browser, a segment rule is waiting for its lookup (at most 800 ms, see Segments);
  • the flag needs something this SDK version cannot evaluate.

A flag that is turned off is not a missing answer: it returns its default variant. Choose a fallback that is safe to ship: usually the current behaviour.

On a server, evaluate(key) explains an answer without counting an exposure. It returns the variant and a reason: STATIC, TARGETING_MATCH, SPLIT, DEFAULT or DISABLED. The reason ERROR means no variant, with the error code NOT_READY (no document yet), FLAG_NOT_FOUND or UNSUPPORTED. A variant can also carry an error code when facts were left out: NOT_ALLOWED (consent, or an experiment counted in the browser) or MEMBERSHIP_UNAVAILABLE (the segment lookup failed).

Targeting facts

Targeting rules test facts about the unit. In the browser the facts are:

  • $utm_source, $utm_medium and $utm_campaign from the page URL, and $referrer_host, the referrer's host name. Each is null when absent;
  • the traits you passed to identify();
  • the properties you set with setFlagProperties(), which win over both.
mira.setFlagProperties({ plan: 'pro', country: 'DE' })

On the script tag: mirafive('flagProperties', { plan: 'pro', country: 'DE' }). On a server, pass properties to the read. Flag properties are held in memory and never sent to MIRA FIVE. Each call to setFlagProperties() replaces the previous properties and notifies onFlags listeners.

Facts only change answers where the SDK evaluates rules: in full mode, and on servers. A consentless page receives the answer for everyone and ignores them.

Segments

A segment rule tests whether the unit is in one of your segments in MIRA FIVE. The SDK asks MIRA FIVE, since the membership lives there:

  • Browser: in full mode with targeting consent, flags are fetched together with the membership of this browser's anonymous id. While that request is out, segment rules wait up to 800 ms, then count as "not in".
  • Server: for() looks up the unit when a flag tests a segment, unless targeting is false or the unit is opted out. Lookups of one tick share one request of up to 100 units, and answers are kept for 1 minute.

Without an answer (no consent, no id, an opt-out, a failed lookup), both "in segment" and "not in segment" conditions are false.

Server-rendered pages

A page rendered on the server and read by the browser SDK afterwards may flip from the fallback to the real answer: the page flickers, or React reports a hydration mismatch. Hand the server's answers to the page in a bootstrap block:

<script type="application/json" id="mirafive-flags">{"v":1,"at":1727430000000,"values":{"new-checkout":["on"]}}</script>

The browser SDK reads it once at start and answers from it at once. It ignores a block older than 7 days, and fetches the flags right away when the block is older than 60 seconds or lists flags only the browser can decide (a split by anonymous id, which the server does not know). The block holds only flags the website reads, never server-only values.

A response carrying the block is per visitor: send it with Cache-Control: private, no-store.

import { bootstrapHeaders } from '@mirafive/sdk-server/flags'

const user = await flags.for({ userId: session.userId })
const html = `<!doctype html><html><head>${user.bootstrap()}</head><body>…</body></html>`

return new Response(html, { headers: { 'Content-Type': 'text/html', ...bootstrapHeaders } })

Print the block before the browser SDK's script. Values are escaped, so the block cannot end the script tag early.

Previews and overrides

To see a variant without being assigned to it, add mirafive-preview to the page URL. It takes flag-key:variant and can repeat:

https://shop.example/pricing?mirafive-preview=pricing-test:b&mirafive-preview=new-checkout:on

On/off flags take on or off. A variant the flag does not have is ignored. Previews apply in full mode, where the page has the flag's rules, and to page experiments. A preview is never counted as an exposure.

A preview link from MIRA FIVE may also carry mirafive-preview-token. The SDK forwards it unchanged; a valid token adds an experiment's unstarted draft to the flags for 24 hours.

In development, pin answers in code with overrides (full mode only, never counted):

flags({ overrides: { 'new-checkout': true, 'pricing-test': 'b' } })

true means on, false means off. The browser answers in this order: override, preview, a page experiment that now shows everyone the original, a page-experiment snippet's decision, then the flag document.

Refresh

SDKFetches the flags
Browser SDKsAt start (unless a fresh bootstrap block covers every flag), every refreshSeconds (default 300) while the tab is visible, when the tab becomes visible, when the URL changes, and when consent or the signed-in user changes what the segment lookup needs
@mirafive/sdk-serverOn a read, at most every refreshSeconds (default 30, at least 10), with If-None-Match. A failed refresh keeps the last document
PHP, Laravel, SymfonyOn a read, at most every 30 seconds (flagsRefreshSeconds), shared through the cache when you pass one

onFlags listeners and React hooks are not notified for a fetch that brings the same flags. An experiment keeps the variant it first showed for the rest of the page load, unless it is turned off. After a 401 or 403, a server SDK stops refreshing until restarted and keeps serving the last document.

SituationWhat flags use
Consentless pageThe answer for everyone. No rollouts by id, no targeting, no experiments
Full mode, no answer yetNo anonymous id, except experiments drawn at random before consent (see Experiments). Flags assigned by person use the user id from identify()
experiments grantedThe anonymous id, for rollouts and experiments; exposures are counted (with statistics)
targeting grantedSegment membership
Do Not Track, GPC, __mirafive_ignoreNo ids, no segments, no exposures. Fixed answers and property rules still apply
Server readconsent.experiments: false uses no anonymous id and counts no experiment; consent.targeting: false looks up no segment

On this page