flags
Turn features on for some users and off for others, with the same answer on the server and in the browser.
Feature flags let you merge unfinished work, turn a feature on for 10% of users, or switch something off in production without a deploy. kitr/flags makes them typed: every flag is declared once with a default, so a misspelt flag is a type error. Providers decide the value per user. The server resolves the flags once per request and hands the browser the same values, so a page never flips after it loads.
Install
kitr add flagsFirst time? Run npm i -g kitr once. Inside a project, the global kitr runs your project's own version.
// app/flags.ts
import { defineFlags } from 'kitr/flags'
export const flags = defineFlags({
newDashboard: { default: false, description: 'The redesigned overview' },
pageSize: { default: 20 },
})
// config/flags.ts
import { defineConfig, envFlagsProvider, percentageFlagsProvider } from 'kitr/flags'
import { flags } from '#app/flags'
export default defineConfig({
definitions: flags,
providers: [envFlagsProvider(), percentageFlagsProvider({ newDashboard: 10 })],
})
The first provider with an answer wins; the default closes the chain. envFlagsProvider reads FLAG_NEW_DASHBOARD=true; percentageFlagsProvider turns a flag on for a stable share of user ids; staticFlagsProvider takes a map or a function of the context; openFeatureFlagsProvider(client, flags) wraps any OpenFeature client.
Read a flag
import { useFlag } from 'kitr/flags/react'
function Overview() {
const fresh = useFlag<typeof flags, 'newDashboard'>('newDashboard')
return fresh ? <NewOverview /> : <Overview />
}
Outside React: useFlags(app).value('pageSize') and isEnabled('newDashboard'). Both are synchronous and read the resolved snapshot.
Same answer on both sides
In the root route's loader, resolve for the request and embed the result; the client provider reads it before the first render:
loader: ({ context }) => resolveFlags(context.app, getRequestEvent()),
head: ({ loaderData }) => ({ scripts: [{ type: 'application/json', id: '__kitr_flags__', children: JSON.stringify(loaderData) }] }),
<FlagsScript values={...} /> from kitr/flags/react renders the same tag. In a pure SPA the provider calls resolve() at boot instead.
Providers
| Provider | Answers from |
|---|---|
envFlagsProvider() |
Environment variables: FLAG_NEW_DASHBOARD=true sets newDashboard. Good for turning things on per environment. |
percentageFlagsProvider({ newDashboard: 10 }) |
A stable share of users, by user id. The same user always gets the same answer. |
staticFlagsProvider(values) |
A fixed map, or a function of the context such as ({ user }) => ({ beta: user?.role === 'staff' }). |
openFeatureFlagsProvider(client, flags) |
Any OpenFeature client: LaunchDarkly, Flagsmith, Unleash, GrowthBook and others. |
They're asked in order, and the first one with an answer wins. A provider is an object with resolve(key, context), so writing one for your own database takes a few lines.
| Option | Default | Description |
|---|---|---|
definitions |
none | The flags from app/flags.ts. |
providers |
none | Asked in order. With none, every flag is its default. |
ttl |
300 |
Seconds the browser keeps its values before refresh() asks again. |
Tests
createFlags(definitions, { providers: [staticFlagsProvider({ newDashboard: true })] }) builds a flags service with fixed answers, so a test can check both sides of a flag.