Flagship Patterns & Best Practices
Evaluating Flags in Workers (Binding)
Simple Boolean Toggle
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const showNewUI = await env.FLAGS.getBooleanValue("new-ui", false, {
userId: "user-42",
});
if (showNewUI) {
return new Response("New UI");
}
return new Response("Classic UI");
},
};Multi-Variant String Flag
const checkoutFlow = await env.FLAGS.getStringValue(
"checkout-flow",
"original",
{ userId, country: "US" },
);
switch (checkoutFlow) {
case "streamlined":
return handleStreamlined(request);
case "one-click":
return handleOneClick(request);
default:
return handleOriginal(request);
}JSON Config Flag
interface RateLimitConfig {
rpm: number;
burst: number;
}
const limits = await env.FLAGS.getObjectValue<RateLimitConfig>(
"rate-limits",
{ rpm: 100, burst: 20 },
{ plan: userPlan },
);Using Details for Observability
const details = await env.FLAGS.getBooleanDetails("new-checkout", false, {
userId: "user-42",
});
console.log(details.value); // true
console.log(details.variant); // "on"
console.log(details.reason); // "TARGETING_MATCH"
console.log(details.errorCode); // undefined (no error)Evaluating Flags with OpenFeature (Workers)
Binding Passthrough (Recommended)
import { OpenFeature } from "@openfeature/server-sdk";
import { FlagshipServerProvider } from "@cloudflare/flagship";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
await OpenFeature.setProviderAndWait(
new FlagshipServerProvider({ binding: env.FLAGS }),
);
const client = OpenFeature.getClient();
const enabled = await client.getBooleanValue("new-checkout", false, {
targetingKey: "user-42",
plan: "enterprise",
country: "US",
});
return new Response(enabled ? "New checkout" : "Standard checkout");
},
};Migration from Another Provider
Only the provider initialization changes — evaluation call sites stay the same:
// ❌ Before (LaunchDarkly)
await OpenFeature.setProviderAndWait(
new LaunchDarklyProvider({ sdkKey: "..." }),
);
// ✅ After (Flagship)
await OpenFeature.setProviderAndWait(
new FlagshipServerProvider({ binding: env.FLAGS }),
);
// Evaluation code is unchanged
const enabled = await client.getBooleanValue("my-flag", false, {
targetingKey: "user-42",
});Managing Flags via REST API
All examples use api.cloudflare.com. Set CLOUDFLARE_ACCOUNT_ID, FLAGSHIP_APP_ID, and CLOUDFLARE_API_TOKEN first.
Create a Boolean Flag
curl -s -X POST \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"key": "new-feature",
"default_variation": "off",
"variations": { "on": true, "off": false },
"rules": [],
"description": "Enable the new feature",
"enabled": false
}' \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags" | jq .Create a Flag with Internal-Only Targeting
curl -s -X POST \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"key": "beta-feature",
"default_variation": "off",
"variations": { "on": true, "off": false },
"rules": [
{
"priority": 1,
"conditions": [
{ "attribute": "email", "operator": "ends_with", "value": "@cloudflare.com" }
],
"serve_variation": "on"
}
],
"description": "Beta feature for internal users",
"enabled": true
}' \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags" | jq .Create a JSON Config Flag
curl -s -X POST \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"key": "rate-limits",
"default_variation": "standard",
"variations": {
"standard": { "rpm": 100, "burst": 20 },
"premium": { "rpm": 1000, "burst": 200 }
},
"rules": [
{
"priority": 1,
"conditions": [
{ "attribute": "plan", "operator": "in", "value": ["enterprise", "business"] }
],
"serve_variation": "premium"
}
],
"description": "Rate limit configuration by plan",
"enabled": true
}' \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags" | jq .Read a Flag
curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags/new-feature" | jq .List All Flags (with pagination)
curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags?limit=50" | jq .If nextCursor is non-null, fetch the next page:
curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags?limit=50&cursor=<nextCursor>" | jq .Update a Flag (Full Replace)
Updates use PUT with the full FlagDefinition. Always GET first, modify, then PUT back.
# 1. Read current flag
FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags/new-feature" | jq '.data')
# 2. Modify (e.g., enable the flag)
UPDATED=$(echo "$FLAG" | jq '.enabled = true')
# 3. PUT back
echo "$UPDATED" | curl -s -X PUT \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d @- \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags/new-feature" | jq .Toggle a Flag On
Read-modify-write to set enabled: true:
BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/new-feature" | jq '.data')
UPDATED=$(echo "$FLAG" | jq '.enabled = true')
echo "$UPDATED" | curl -s -X PUT \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d @- "$BASE/new-feature" | jq .Toggle a Flag Off (Disable)
Same pattern, set enabled: false. The flag immediately returns its default variation for all evaluations.
BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/new-feature" | jq '.data')
UPDATED=$(echo "$FLAG" | jq '.enabled = false')
echo "$UPDATED" | curl -s -X PUT \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d @- "$BASE/new-feature" | jq .Add a Targeting Rule to an Existing Flag
Append a rule to the existing rules array. Pick a priority that doesn't collide with existing rules.
BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/new-feature" | jq '.data')
UPDATED=$(echo "$FLAG" | jq '.rules += [{
"priority": 2,
"conditions": [{ "attribute": "plan", "operator": "equals", "value": "enterprise" }],
"serve_variation": "on"
}]')
echo "$UPDATED" | curl -s -X PUT \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d @- "$BASE/new-feature" | jq .Change Rollout Percentage
Update the rollout percentage on an existing rule (e.g., rule at index 0):
BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/gradual-rollout" | jq '.data')
UPDATED=$(echo "$FLAG" | jq '.rules[0].rollout.percentage = 50')
echo "$UPDATED" | curl -s -X PUT \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d @- "$BASE/gradual-rollout" | jq .Change Default Variation
BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/new-feature" | jq '.data')
UPDATED=$(echo "$FLAG" | jq '.default_variation = "on"')
echo "$UPDATED" | curl -s -X PUT \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d @- "$BASE/new-feature" | jq .Add a New Variation
BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/checkout-flow" | jq '.data')
UPDATED=$(echo "$FLAG" | jq '.variations["treatment-c"] = "minimal"')
echo "$UPDATED" | curl -s -X PUT \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d @- "$BASE/checkout-flow" | jq .Remove a Rule
Remove a rule by filtering on priority:
BASE="https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags"
FLAG=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "$BASE/new-feature" | jq '.data')
UPDATED=$(echo "$FLAG" | jq '.rules = [.rules[] | select(.priority != 2)]')
echo "$UPDATED" | curl -s -X PUT \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d @- "$BASE/new-feature" | jq .Delete a Flag
curl -s -X DELETE \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps/$FLAGSHIP_APP_ID/flags/old-feature" | jq .Targeting Rule Patterns
Enterprise-Only Access
{
"priority": 1,
"conditions": [
{ "attribute": "plan", "operator": "equals", "value": "enterprise" }
],
"serve_variation": "on"
}Country-Based Targeting with Logical AND/OR
Target enterprise users in the US or Canada:
{
"priority": 1,
"conditions": [
{
"logical_operator": "AND",
"clauses": [
{ "attribute": "plan", "operator": "equals", "value": "enterprise" },
{
"logical_operator": "OR",
"clauses": [
{ "attribute": "country", "operator": "equals", "value": "US" },
{ "attribute": "country", "operator": "equals", "value": "CA" }
]
}
]
}
],
"serve_variation": "on"
}Percentage Rollout
Gradually roll out to 10% of users:
{
"priority": 1,
"conditions": [
{ "attribute": "targetingKey", "operator": "not_equals", "value": "" }
],
"serve_variation": "on",
"rollout": {
"percentage": 10,
"attribute": "targetingKey"
}
}Progressive Rollout Workflow
- Create flag with 5% rollout, enable it
- Monitor metrics
- Increase to 25% → 50% → 100% by updating the
rollout.percentage - Once at 100%, remove the rule and set
default_variationto the winning variation - Eventually remove the flag and the code branch
Safe Deletion Workflow
- Disable the flag first (
enabled: false) — confirms nothing depends on it being active - Monitor for unexpected behavior
- Remove flag evaluation code from your application
- Deploy the code change
- Delete the flag via API