Klor.

Your build is frozen.
Your app doesn’t have to be.

Remote config, feature flags, and update gating for React and React Native. Install the package, paste a key, and change what your app does in seconds: no release, no review, no waiting for people to update.

pnpm add @klor/react
prodAurora iOS

checkout_v2

boolean · 2 rules · read 4s ago

{
  "checkout_v2": true,
  "max_basket_items": 25,
  "payment_methods": ["card", "apple_pay"]
}
#0042live
9:41Aurora

Checkout

Pay with Apple Pay
Use a card
Sourdough loaf£4.20
Delivery£1.99
Total£6.19

One tap. No account needed.

For your agent

Implement with your agent in a few minutes.

Copy this into Claude Code, Cursor, or whatever you use. It covers the install, the provider, the rules that are easy to get wrong, and the two extra options React Native needs, and it ends by telling your agent to ask you for the key rather than invent one.

Read it first
Paste into your agent
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.

Remote config

max_basket_items

Any value your app reads (a number, a string, a whole JSON blob), kept outside the binary. Change it in the dashboard, publish, and every running app picks it up on its next refresh.

{
"max_basket_items": 25,
"support_email": "help@aurora.app",
"promo": { "code": "SPRING", "percent": 15 }
}

Feature flags

paywall_variant

Ordered rules, matched top to bottom. Target a platform, an app version, a country, anything you attach to the user, then release to a percentage of whoever matches. Bucketing is stable, so nobody flickers between variants and raising 10% to 20% only ever adds people.

rollout10%

Bucketed by userId. The same person lands in the same bucket on every launch and on every platform.

Update gating

min_supported_version

The mobile-only one. Set a floor and a latest version per platform, or pull a single bad build from circulation mid-incident. Klor returns a verdict and the copy to show; you render the prompt, so it looks like your app.

Update required

Version 2.3.1 has a problem with saved cards. Update to keep using the app.

Update

status: forced · reason: blocked

Three lines

One package, three places to read from.

The same evaluation engine runs in the browser, on the device, and on your server, so a flag cannot mean one thing in your app and another in your API.

import { KlorProvider, createKlorClient, useFlag } from '@klor/react'

const klor = createKlorClient({ apiKey: 'klor_pub_…' })

function Checkout() {
const newCheckout = useFlag('checkout_v2', false)
return newCheckout ? <OneTapCheckout /> : <CardForm />
}

A config service you can afford to depend on.

Evaluated on the device

The SDK downloads one payload and decides locally. Reading a flag is synchronous, no request, no loading state, no flash of the wrong thing.

Works with the network off

The last payload is cached to disk. A cold start on a dead connection serves real values, not fallbacks.

Survives our bad day

Published snapshots are static JSON on Cloudflare’s edge. Reads never touch our database, so a dashboard outage is not your outage.

Two kinds of key

Public keys ship in your app and never see flags marked sensitive. Private keys stay on your server and see everything.

Stop shipping releases to change a number.