SDKs
Every SDK evaluates flags in your own process, from a copy it keeps up to date, so your users’ attributes never leave it and the same user gets the same answer in every language.
TypeScript
Browsers, Node, Cloudflare Workers and Vercel Edge. Zero dependencies, about 12 KB gzipped.
Install
npm install @featflags/jsUse
import { FlagClient } from '@featflags/js'
const flags = new FlagClient({
key: 'pk_live_…',
cdnUrl: 'https://flags.featflags.io',
context: { kind: 'user', key: userId, plan: 'pro' },
})
if (flags.bool('new-checkout', false)) showNewCheckout()React
Provider and hooks, with server rendering.
Install
npm install @featflags/reactUse
import { FeatflagsProvider, useFlag } from '@featflags/react'
<FeatflagsProvider options={{ key: 'pk_live_…', cdnUrl: 'https://flags.featflags.io' }} context={{ key: user.id }}>
<App />
</FeatflagsProvider>
function Checkout() {
return useFlag('new-checkout', false) ? <NewCheckout /> : <OldCheckout />
}React Native
React Native 0.74+, with the React hooks. Starts from the last flags it saw, even offline.
Install
npm install @featflags/react-native @react-native-async-storage/async-storage
npm install @react-native-community/netinfo # optional: refetch as soon as the network is back
cd ios && pod installUse
import { createClient, FeatflagsProvider, useFlag, type FlagClient } from '@featflags/react-native'
const flags = createClient({ key: 'pk_live_…', cdnUrl: 'https://flags.featflags.io' })
export function App() {
const [client, setClient] = useState<FlagClient>()
useEffect(() => void flags.then(setClient), [])
if (!client) return null
return <FeatflagsProvider client={client} context={{ key: user.id }}><Home /></FeatflagsProvider>
}
function Checkout() {
return useFlag('new-checkout', false) ? <NewCheckout /> : <OldCheckout />
}Go
Standard library only. OpenFeature provider in its own module.
Install
go get github.com/featflags/featflags-goUse
flags, err := featflags.New(ctx, featflags.Config{
Key: "sk_live_…",
CDNURL: "https://flags.featflags.io",
})
user := featflags.NewContext(map[string]any{"key": userID, "plan": "pro"})
if flags.Bool(ctx, "new-checkout", user, false) { … }Python
Standard library only. OpenFeature provider behind an extra.
Install
pip install featflags
pip install 'featflags[openfeature]' # with the OpenFeature providerUse
flags = featflags.Client("sk_live_…", "https://flags.featflags.io")
if flags.bool_variation("new-checkout", {"key": user_id, "plan": "pro"}, False):
...Java
Java 17+ and Kotlin on servers, Jackson only. OpenFeature provider included.
Install
implementation("io.featflags:featflags-java:0.1.0")Use
var flags = FeatflagsClient.builder("sk_live_…", "https://flags.featflags.io").build();
if (flags.boolVariation("new-checkout", Context.of(Map.of("key", userId)), false)) { … }Rust
Async, with an on-disk cache so your service starts even when we can’t be reached.
Install
cargo add featflagsUse
let flags = featflags::Client::builder("https://flags.featflags.io", "sk_live_…")
.build()
.await;
let ctx = featflags::context(json!({ "key": user_id, "plan": "pro" }));
if flags.bool("new-checkout", &ctx, false) { … }.NET
.NET 8+, base class library only. OpenFeature provider included.
Install
dotnet add package FeatFlagsUse
await using var flags = await FeatFlagsClient.CreateAsync(new FeatFlagsOptions
{
Key = "sk_live_…",
CdnUrl = "https://flags.featflags.io",
});
var user = Context.Of(new Dictionary<string, object?> { ["key"] = userId, ["plan"] = "pro" });
if (flags.BoolVariation("new-checkout", user, false)) { … }Ruby
Standard library only. Works under Puma and Unicorn without extra setup.
Install
bundle add featflagsUse
flags = Featflags::Client.new("sk_live_…", "https://flags.featflags.io")
if flags.bool_variation("new-checkout", { key: user.id, plan: "pro" }, false)
# …
endPHP
PHP 8.2+. A request never waits for us once your servers have the flags.
Install
composer require featflags/featflagsUse
$flags = new Featflags\Client('sk_live_…', 'https://flags.featflags.io');
if ($flags->boolVariation('new-checkout', ['key' => $userId, 'plan' => 'pro'], false)) {
// …
}Elixir
Add it to your supervision tree. Flags are read in the calling process.
Install
{:featflags, "~> 0.1"} # in mix.exs depsUse
children = [
{Featflags, name: MyApp.Flags, key: "sk_live_…", cdn_url: "https://flags.featflags.io"}
]
if Featflags.bool_variation(MyApp.Flags, "new-checkout", %{key: user.id}, false) do
# …
endiOSSwift
Swift, for iOS and macOS. Starts from the last flags it saw, even offline.
Install
.package(url: "https://github.com/featflags/featflags-swift", from: "0.1.0") // or in Xcode: File → Add Package DependenciesUse
var options = FlagClient.Options(key: "pk_live_…", cdnURL: URL(string: "https://flags.featflags.io")!)
options.context = ["key": user.id, "plan": "pro"]
let flags = FlagClient(options)
if flags.bool("new-checkout", default: false) { … }AndroidKotlin
Kotlin, minSdk 24. Starts from the last flags it saw, even offline.
Install
implementation("io.featflags.kotlin:featflags-android:0.1.0")Use
val flags = FeatflagsClient.android(app, FeatflagsOptions(key = "pk_live_…", cdnUrl = "https://flags.featflags.io"))
flags.identify(Context.user(user.id, mapOf("plan" to "pro")))
if (flags.bool("new-checkout", false)) { … }FlutterDart
Dart, for Flutter apps, Flutter web and Dart programs. Widgets rebuild when a flag changes.
Install
flutter pub add featflags_flutter
dart pub add featflags # a Dart program without FlutterUse
final flags = FlagClient(FlagClientOptions(
key: 'pk_live_…',
cdnUrl: Uri.parse('https://flags.featflags.io'),
context: {'key': user.id, 'plan': 'pro'},
));
runApp(FeatFlagsProvider(client: flags, child: const App()));
// In build:
FeatFlagsProvider.of(context).boolValue('new-checkout', false) ? const NewCheckout() : const Checkout()OpenFeature
The Go, Python, Java, .NET, Ruby, PHP, Elixir, iOS, Android and Flutter SDKs come with an OpenFeature provider. Code written against the OpenFeature API reads featflags flags once you register it, so you can switch to featflags, or away from it, without touching the call sites.
AI assistants (MCP)
Connect Claude, Cursor or another assistant to the hosted MCP server and it can look up, explain, create and change your flags. There’s nothing to install and no key to copy: add the URL, sign in with your featflags account when the assistant asks, then pick the organization it works in and whether it may change things or only read them.
https://app.featflags.io/mcpClaude Code
Run this, then /mcp in Claude Code to sign in:
claude mcp add --transport http featflags https://app.featflags.io/mcpClaude
On claude.ai or in the desktop app: Settings → Connectors → Add custom connector, and paste the URL.
Cursor
Settings → MCP → Add new MCP server, or in ~/.cursor/mcp.json:
{ "mcpServers": { "featflags": { "url": "https://app.featflags.io/mcp" } } }VS Code
Command Palette → MCP: Add Server → HTTP, or in .vscode/mcp.json:
{ "servers": { "featflags": { "type": "http", "url": "https://app.featflags.io/mcp" } } }
The assistant acts as you, so its changes need the same comments and approvals as yours and show up in each flag’s history. Disconnect it any time under Connected apps on your account page.
CLI
The featflags command works with projects, flags, their history and uploaded customer lists from a terminal or CI. featflags login signs you in through the app in your browser; in CI, set FEATFLAGS_API_URL to https://app.featflags.io and FEATFLAGS_TOKEN to an API token from Settings → API tokens instead. FEATFLAGS_PROJECT saves repeating --project.
featflags login --api-url https://app.featflags.io # opens the app to pick an organization
featflags flags list --project acme
featflags flags toggle new-checkout --project acme --env production --off --comment "incident 4211"
featflags lists upload customers.txt --project acme --segment customers --list includedTerraform
The Terraform provider manages projects, environments, SDK keys, flags and their targeting rules, segments, release templates and webhooks as code. It signs in with an API token, whose roles decide what it can change. Rules you add in the app live alongside the ones Terraform manages, and an environment that needs approval holds Terraform’s changes until someone approves them.
terraform {
required_providers {
featflags = { source = "featflags/featflags" }
}
}
provider "featflags" {
api_url = "https://app.featflags.io" # the token comes from FEATFLAGS_TOKEN
}Importing from LaunchDarkly
The CLI copies a LaunchDarkly project into featflags: environments, context kinds, flags with their targeting, segments and big segments. Bucketing is compatible, so every user keeps the variant they had. Start with --dry-run to see what it would do, then import and pass --verify a file of sample contexts, one JSON object per line, to check that every flag gives the same answer as LaunchDarkly. Running it again only writes what changed, so you can keep both in step while you switch SDKs.
export LD_API_TOKEN=api-… # a LaunchDarkly access token that can read the project
featflags import launchdarkly --ld-project acme --to-project acme --dry-run
featflags import launchdarkly --ld-project acme --to-project acme --verify contexts.jsonlExperiments
An experiment splits part of a flag’s traffic between its variants and shows which did better on the metrics you choose. First generate the environment’s experiment key in its settings: it’s created in your browser and shown once, for your servers to hold, and we never see it, so the IDs your SDKs report can’t be traced back to a person without it. Then add metrics on the project’s Metrics page, send the events they count with your SDK’s track(), and start the experiment from a rule or the default in a flag’s targeting with Run an experiment.
Release templates
A release template is a rollout you write once and apply to any flag: staff first, then 10%, 50% and everyone, each step started by hand or after a set time. Create one on the project’s Release templates page, starting from the Gradual or Staff first preset, and apply it from a flag’s Release tab. On Team and Business, a release can also watch your metrics while each step is live, and pause or go back a step when the new variant does worse.