Your build is frozen.Your app doesn’t have to be.
Remote config, feature flags, and update gating for React and React Native. Change what a released app does, for some people or for everyone, without a new build.
Each dot is a phone running your app. Drag to stage a rollout, then publish it and watch it reach them.
Checkout
The circled phone
#0042 is live
Or hand it to your agent.
Paste this into Claude Code, Cursor, or whatever you use. It covers the install, the provider, the rules that are easy to get wrong, and what React Native needs, and it tells your agent to ask you for the key instead of inventing one.
Integrate Klor (klor.dev) into this project for remote config and feature flags.
Package: @klor/react, one package, works in React and React Native.
## Steps
1. Install it:
pnpm add @klor/react
2. Create the client once. It owns the refresh schedule, the disk cache, and
the telemetry buffer, so creating it per render would restart all three.
import { createKlorClient } from '@klor/react'
export const klor = createKlorClient({ apiKey: KLOR_PUBLIC_KEY })
Module scope is the simplest way to get "once". If you are server-rendering
and want to seed it with a snapshot the server already fetched, create it in
a useState initialiser instead, still once per mount, not per render:
const [klor] = useState(() =>
createKlorClient({ apiKey: KLOR_PUBLIC_KEY, initialSnapshot }),
)
3. Wrap the app root. The context is who the current user is; rules are matched
against it.
import { KlorProvider } from '@klor/react'
<KlorProvider
client={klor}
context={{ userId: user.id, attributes: { platform: 'web', country: user.country } }}
>
<App />
</KlorProvider>
4. Read flags where they are used:
import { useFlag } from '@klor/react'
const showNewCheckout = useFlag('checkout_v2', false)
## Rules
- The second argument to useFlag is the value served when Klor has nothing to
say, before the first fetch, on a dead network, or if the key does not exist.
Choose the behaviour you want if Klor were not installed at all.
- useFlag is synchronous and already returns that fallback. Do not gate the UI
on a loading state, and do not wrap it in useMemo or component state.
- Keys beginning klor_pub_ are public and may ship in client code. Keys
beginning klor_sec_ must never appear in anything sent to a browser or bundled
into an app, server only, through @klor/react/server.
- Flags marked sensitive in the dashboard are stripped from the payload public
keys receive. Do not rely on a public key to hide anything.
## React Native only
@klor/react has no react-native dependency, so two things must be passed in:
import AsyncStorage from '@react-native-async-storage/async-storage'
import { AppState } from 'react-native'
createKlorClient({
apiKey: KLOR_PUBLIC_KEY,
storage: AsyncStorage,
subscribeToForeground: (refresh) => {
const sub = AppState.addEventListener('change', (s) => s === 'active' && refresh())
return () => sub.remove()
},
})
Without storage, a cold start with no network serves fallbacks instead of the
last known config, which on mobile is the case most worth covering.
For update gating, useVersionGate() returns { status, message, storeUrl }. Klor
ships no UI; render your own prompt from it.
## If you need more
The complete reference (every client option, every rule operator, the version
comparison rules, and the HTTP endpoints) is one fetch away as plain text:
https://klor.dev/llms-full.txt
There is also https://klor.dev/llms.txt, a short index linking each docs page,
if you would rather read only the part you need.
## Before you start
Ask me for the Klor public API key and the flag keys I want to read. Do not
invent key names or commit a placeholder that looks real.Three things you can change after release.
Any value your app reads
Numbers, strings, whole JSON objects, kept outside the binary. Change one in the dashboard, publish, and running apps pick it up on their next refresh.
{
"max_basket_items": 25,
"support_email": "help@aurora.app",
"promo": { "code": "SPRING", "percent": 15 }
}Who sees a feature
Rules are checked top to bottom and the first match wins. Target a platform, a version, a country, anything you know about the user, then release to a share of them. Raising 10% to 40% only ever adds people.
- 1platform is ios, and app version is 2.4.0 or latertrue, for 40%
- 2country is TR or DEtrue
- Everyone elsefalse
Who has to update
Set a minimum and a latest version per platform, or pull one bad build mid incident. Klor tells the app what to show and you draw the prompt, so it looks like your app.
This exact build was pulled, though it clears the minimum.
Update required
This version of Aurora needs an update before you can keep ordering.
One package for the browser, the phone, and the server.
The same evaluation code runs in all three, so a flag cannot mean one thing in your app and another in your API.
pnpm add @klor/reactimport { KlorProvider, createKlorClient, useFlag } from '@klor/react'
const klor = createKlorClient({ apiKey: 'klor_pub_…' })
function Checkout() {
const newCheckout = useFlag('checkout_v2', false)
return newCheckout ? <OneTapCheckout /> : <CardForm />
}When something goes wrong.
- The phone has no signal at launch.
- It reads the last configuration from disk and opens with real values, not fallbacks.
- Our dashboard is down.
- Apps read published snapshots from Cloudflare’s edge, which never touches our database. Yours carries on.
- Someone unpacks your app.
- They find a public key. It reads only the public payload, and flags you mark sensitive are never in it.
- A flag is read on every render.
- Reading is a lookup in memory on the device. No request, no loading state, no flicker.