config(git,claude,ssh): templated gitconfig; claude settings/commands/hooks/skills; ssh config

gitconfig: name/email from chezmoi data, gh credential helper by PATH
lookup (portable to darwin). claude: no credentials, history, projects,
sessions, or plugin caches. ssh config as private_ (0600).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Prad NukalaandClaude Fable 5 committed 2026-08-09 14:53:46 -04:00
1 parent 3c691e8dc5
commit 20891f3910
476 files changed
+59086 -7

No files matched your search

@@ -0,0 +1,59 @@
# Cloudflare Flagship
Feature flag service for controlling feature visibility without redeploying code. Define flags with targeting rules and percentage-based rollouts, then evaluate them in Workers via a native binding or from any JavaScript runtime via the OpenFeature SDK.
## When to Use
| Need | Use Flagship? | Alternative |
|------|--------------|-------------|
| Feature toggles (on/off) | Yes | — |
| Gradual rollouts (percentage-based) | Yes | — |
| A/B testing with attribute targeting | Yes | — |
| Multi-variant configuration delivery | Yes | — |
| Environment-specific config (dev/staging/prod) | Consider | Wrangler environments, secrets |
| Static config that never changes | No | `wrangler.jsonc` vars |
| Per-request rate limiting | No | Rate Limiting rules |
## Key Concepts
- **Apps** — Top-level organizational unit. Maps to a project or service. Each account can have multiple apps.
- **Flags** — Named feature toggles with a key, variations, targeting rules, and enabled/disabled state.
- **Variations** — Possible values a flag returns. Types: boolean, string, number, JSON object. All variations on a flag must share the same type.
- **Targeting rules** — Sequential, priority-ordered conditions that determine which variation to serve. First match wins; no match returns the default.
- **Evaluation context** — Key-value attributes (`userId`, `country`, `plan`, etc.) passed at evaluation time for rule matching and rollout bucketing.
- **Percentage rollouts** — Gradually release to a fraction of users. Consistent hashing on a configurable attribute ensures sticky bucketing.
## Two Evaluation Paths
| Path | Runtime | Package | Latency | Auth |
|------|---------|---------|---------|------|
| **Binding** (`env.FLAGS`) | Workers only | `@cloudflare/workers-types` | Lowest (no HTTP) | Automatic via binding |
| **OpenFeature SDK** | Workers, Node.js, browser | `@cloudflare/flagship` + `@openfeature/server-sdk` or `@openfeature/web-sdk` | HTTP per eval (server) or prefetch (client) | API token or binding passthrough |
**Recommendation:** Use the binding inside Workers. Use the SDK when running outside Workers or when you need OpenFeature vendor-neutrality.
## Reading Order
| Task | Read |
|------|------|
| Set up Flagship in a Worker | `configuration.md` → `api.md` |
| Evaluate flags in code | `configuration.md` → `patterns.md` |
| Manage flags via REST API | `api.md` → `patterns.md` |
| Design targeting rules & rollouts | `patterns.md` → `gotchas.md` |
| Debug flag evaluation issues | `gotchas.md` → `api.md` |
REST API note: management endpoints use Cloudflare v4 envelopes (`result`, `result_info`, `errors`) and snake_case fields. The `/evaluate` endpoint is the exception: it is not enveloped and returns OpenFeature-style camelCase.
## In This Reference
- **[api.md](./api.md)** — REST API endpoints, binding methods, OpenFeature SDK, schemas
- **[configuration.md](./configuration.md)** — Wrangler binding setup, SDK installation, TypeScript types
- **[patterns.md](./patterns.md)** — Flag CRUD via API, targeting rules, rollouts, OpenFeature usage
- **[gotchas.md](./gotchas.md)** — Common errors, limits, anti-patterns, troubleshooting
## See Also
- **[Flagship API reference](https://developers.cloudflare.com/api/resources/flagship/)** — Source of truth for REST API paths, envelopes, and response fields
- **[../workers/](../workers/)** — Workers runtime (Flagship runs inside Workers)
- **[../kv/](../kv/)** — KV storage (Flagship uses KV infrastructure for flag delivery)
- **[../wrangler/](../wrangler/)** — Wrangler CLI for deployment and config
@@ -0,0 +1,390 @@
# Flagship API Reference
## Binding API (Workers)
The binding is available as `env.FLAGS` (type `Flagship` from `@cloudflare/workers-types`).
### Evaluation Methods
All methods are async, never throw, and return the `defaultValue` on errors.
| Method | Signature | Returns |
|--------|-----------|---------|
| `get` | `get(flagKey, defaultValue?, context?)` | `Promise<unknown>` |
| `getBooleanValue` | `getBooleanValue(flagKey, defaultValue, context?)` | `Promise<boolean>` |
| `getStringValue` | `getStringValue(flagKey, defaultValue, context?)` | `Promise<string>` |
| `getNumberValue` | `getNumberValue(flagKey, defaultValue, context?)` | `Promise<number>` |
| `getObjectValue` | `getObjectValue<T>(flagKey, defaultValue, context?)` | `Promise<T>` |
| `getBooleanDetails` | `getBooleanDetails(flagKey, defaultValue, context?)` | `Promise<FlagshipEvaluationDetails<boolean>>` |
| `getStringDetails` | `getStringDetails(flagKey, defaultValue, context?)` | `Promise<FlagshipEvaluationDetails<string>>` |
| `getNumberDetails` | `getNumberDetails(flagKey, defaultValue, context?)` | `Promise<FlagshipEvaluationDetails<number>>` |
| `getObjectDetails` | `getObjectDetails<T>(flagKey, defaultValue, context?)` | `Promise<FlagshipEvaluationDetails<T>>` |
### Parameters (shared across all methods)
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `flagKey` | `string` | Yes | Flag key to evaluate |
| `defaultValue` | varies | Yes (except `get`) | Fallback if evaluation fails or flag not found |
| `context` | `FlagshipEvaluationContext` | No | Attributes for targeting rules (`{ userId: "user-42", country: "US" }`) |
### Types
```typescript
type FlagshipEvaluationContext = Record<string, string | number | boolean>;
interface FlagshipEvaluationDetails<T> {
flagKey: string;
value: T;
variant?: string; // name of the matched variation
reason?: string; // "TARGETING_MATCH" | "DEFAULT" | "DISABLED" | "SPLIT"
errorCode?: string; // "TYPE_MISMATCH" | "GENERAL"
errorMessage?: string;
}
```
### Example
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const enabled = await env.FLAGS.getBooleanValue("new-feature", false, {
userId: "user-42",
});
return new Response(enabled ? "Feature on" : "Feature off");
},
};
```
---
## OpenFeature SDK
Package: `@cloudflare/flagship`
### Server Provider (`FlagshipServerProvider`)
For Workers, Node.js, and server-side JavaScript.
**With binding (recommended inside Workers):**
```typescript
import { OpenFeature } from "@openfeature/server-sdk";
import { FlagshipServerProvider } from "@cloudflare/flagship";
await OpenFeature.setProviderAndWait(
new FlagshipServerProvider({ binding: env.FLAGS }),
);
const client = OpenFeature.getClient();
const enabled = await client.getBooleanValue("new-checkout", false, {
targetingKey: "user-42",
});
```
**With app ID (Node.js / non-Worker runtimes):**
```typescript
import { OpenFeature } from "@openfeature/server-sdk";
import { FlagshipServerProvider } from "@cloudflare/flagship";
await OpenFeature.setProviderAndWait(
new FlagshipServerProvider({
appId: "<APP_ID>",
accountId: "<ACCOUNT_ID>",
authToken: "<API_TOKEN>",
}),
);
const client = OpenFeature.getClient();
const enabled = await client.getBooleanValue("new-checkout", false, {
targetingKey: "user-42",
});
```
### Client Provider (`FlagshipClientProvider`)
For browser applications. Pre-fetches flags on init, evaluates synchronously.
```typescript
import { OpenFeature } from "@openfeature/web-sdk";
import { FlagshipClientProvider } from "@cloudflare/flagship";
await OpenFeature.setProviderAndWait(
new FlagshipClientProvider({
appId: "<APP_ID>",
accountId: "<ACCOUNT_ID>",
authToken: "<API_TOKEN>",
prefetchFlags: ["promo-banner", "dark-mode"],
}),
);
await OpenFeature.setContext({ targetingKey: "user-42", plan: "enterprise" });
const client = OpenFeature.getClient();
// Synchronous — no await needed
const showBanner = client.getBooleanValue("promo-banner", false);
```
**Important:** Only flags listed in `prefetchFlags` are available. Unlisted flags return `FLAG_NOT_FOUND`.
### SDK Hooks
```typescript
import { LoggingHook, TelemetryHook } from "@cloudflare/flagship";
OpenFeature.addHooks(new LoggingHook(), new TelemetryHook());
```
---
## REST API (Flag Management)
Source of truth: [Cloudflare Flagship API reference](https://developers.cloudflare.com/api/resources/flagship/). Use it to verify REST paths, envelopes, response fields, and permission wording before relying on examples here.
### FIRST: Check Prerequisites
Before making any REST API calls (create, read, update, delete, toggle flags), verify these environment variables are set:
| Variable | Purpose | How to get |
|----------|---------|------------|
| `CLOUDFLARE_ACCOUNT_ID` | Account identifier | Dashboard URL or `wrangler whoami` |
| `CLOUDFLARE_API_TOKEN` | Bearer token for API auth | [Create API token](https://dash.cloudflare.com/profile/api-tokens) with Flagship permissions |
| `FLAGSHIP_APP_ID` | Target app UUID | Dashboard under **Compute > Flagship**, or `GET /apps` endpoint |
Check with:
```bash
echo "CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID:-(not set)}"
echo "CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN:-(not set)}"
echo "FLAGSHIP_APP_ID=${FLAGSHIP_APP_ID:-(not set)}"
```
**If any are missing, ask the user to provide them before proceeding.**
### Base URL and Auth
Base URL: `https://api.cloudflare.com/client/v4/accounts/{account_id}/flagship`
Authentication: `Authorization: Bearer <API_TOKEN>`
Management endpoints use the Cloudflare v4 envelope. On success, the payload is under `result`; errors are an array under `errors`.
```jsonc
// Success
{ "success": true, "result": <T>, "errors": [], "messages": [] }
// Paginated success
{
"success": true,
"result": [<T>],
"result_info": { "count": 50, "cursor": "next-cursor-or-null" },
"errors": [],
"messages": []
}
// Error
{ "success": false, "result": null, "errors": [{ "message": "message" }], "messages": [] }
```
### App Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/apps` | List all apps |
| `GET` | `/apps/{app_id}` | Get app |
| `POST` | `/apps` | Create app (`{ "name": "my-app" }`) |
| `PUT` | `/apps/{app_id}` | Update app (`{ "name": "new-name" }`) |
| `DELETE` | `/apps/{app_id}` | Delete app |
App name constraints: alphanumeric + hyphens + underscores, 1-64 chars.
### Flag Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/apps/{app_id}/flags?limit=50&cursor=<cursor>` | List flags (paginated) |
| `GET` | `/apps/{app_id}/flags/{flag_key}` | Get flag |
| `POST` | `/apps/{app_id}/flags` | Create flag |
| `PUT` | `/apps/{app_id}/flags/{flag_key}` | Update flag (full replace) |
| `DELETE` | `/apps/{app_id}/flags/{flag_key}` | Delete flag |
| `GET` | `/apps/{app_id}/flags/{flag_key}/changelog?limit=20&cursor=<cursor>` | Flag changelog |
### Evaluate Endpoint
```
GET /apps/{app_id}/evaluate?flagKey=<key>&<context-attrs>
```
Requires an API token with the `com.cloudflare.account.flagship.evaluate` permission. Context attributes passed as query params. This endpoint is not wrapped in the management envelope; the SDK contract returns OpenFeature-style camelCase:
```json
{
"flagKey": "my-flag",
"value": true,
"variant": "on",
"reason": "SPLIT"
}
```
Reasons: `TARGETING_MATCH`, `SPLIT`, `DEFAULT`, `DISABLED`.
### Management Response Payloads
Management endpoints are wrapped in the Cloudflare v4 envelope shown above. Common `.result` payloads:
**App result**
```json
{
"id": "app-uuid",
"name": "my-app",
"created_at": "2026-06-09T12:00:00.000Z",
"updated_at": "2026-06-09T12:00:00.000Z",
"updated_by": "user@example.com"
}
```
**Flag result**
```json
{
"key": "my-flag",
"type": "boolean",
"default_variation": "off",
"variations": { "on": true, "off": false },
"rules": [],
"description": "Enables the new feature",
"enabled": true,
"updated_at": "2026-06-09T12:00:00.000Z",
"updated_by": "user@example.com"
}
```
**Changelog entry**
```json
{
"flag_key": "my-flag",
"event": "update",
"after": { "key": "my-flag", "default_variation": "off", "variations": { "on": true, "off": false }, "rules": [], "enabled": true },
"diff": { "enabled": { "from": false, "to": true } }
}
```
Changelog entries include the full flag state after the change. `update` entries also include `diff`.
---
## FlagDefinition Schema
```json
{
"key": "my-flag",
"type": "boolean",
"default_variation": "off",
"variations": {
"on": true,
"off": false
},
"rules": [
{
"priority": 1,
"conditions": [
{
"attribute": "email",
"operator": "ends_with",
"value": "@cloudflare.com"
}
],
"serve_variation": "on",
"rollout": { "percentage": 100 }
}
],
"description": "Enables the new feature",
"enabled": true
}
```
### Field Constraints
| Field | Type | Constraints |
|-------|------|-------------|
| `key` | string | 1-64 chars, `/^[a-zA-Z0-9_-]+$/` |
| `type` | enum | Optional. `boolean`, `string`, `number`, `json` (auto-inferred from variations) |
| `default_variation` | string | Must be a key in `variations` |
| `variations` | `Record<string, T>` | At least one. All values same type. Keys: alphanumeric/hyphens/underscores, max 64 chars. Values max 10KB. |
| `rules` | `Rule[]` | Can be empty. No duplicate priorities. |
| `description` | string? | Max 512 chars, nullable |
| `enabled` | boolean | Required. `false` = always returns default variation. |
### Rule Schema
```json
{
"priority": 1,
"conditions": [ /* Condition[] */ ],
"serve_variation": "on",
"rollout": { "percentage": 50, "attribute": "targetingKey" }
}
```
- `priority`: integer >= 1, unique across rules in the flag (lower = evaluated first)
- `conditions`: array of base or logical conditions
- `serve_variation`: must be a key in `variations`
- `rollout`: optional. `percentage` 0-100. `attribute` defaults to `targetingKey`.
### Condition Schema
**Base condition:**
```json
{ "attribute": "email", "operator": "ends_with", "value": "@cloudflare.com" }
```
**Logical condition (AND/OR):**
```json
{
"logical_operator": "AND",
"clauses": [
{ "attribute": "country", "operator": "equals", "value": "US" },
{ "attribute": "plan", "operator": "in", "value": ["enterprise", "business"] }
]
}
```
Nesting supported up to 6 levels deep.
### Operators
| Operator | Description | Value Type |
|----------|-------------|------------|
| `equals` | Exact match (case-sensitive) | String |
| `not_equals` | Not exact match | String |
| `greater_than` | Numeric / datetime > | Number, ISO 8601 |
| `less_than` | Numeric / datetime < | Number, ISO 8601 |
| `greater_than_or_equals` | >= | Number, ISO 8601 |
| `less_than_or_equals` | <= | Number, ISO 8601 |
| `contains` | Substring match (case-sensitive) | String |
| `starts_with` | Prefix match | String |
| `ends_with` | Suffix match | String |
| `in` | Value in array | Array |
| `not_in` | Value not in array | Array |
---
## Rate Limits
| Operation | Limit |
|-----------|-------|
| Mutations (POST/PUT/DELETE) | 60 per 60s per account:app |
| Reads (GET) | 600 per 60s per account:app |
## Error Codes
| HTTP Status | Meaning |
|-------------|---------|
| 200 | Success (read/update/delete) |
| 201 | Created (create) |
| 400 | Validation error (check `errors[].message`) |
| 401 | Invalid or missing token |
| 404 | Flag or app not found |
| 409 | Flag key already exists (create) |
| 429 | Rate limited |
@@ -0,0 +1,202 @@
# Flagship Configuration
## Wrangler Binding Setup
Add a Flagship binding to your Wrangler config to access flags via `env.FLAGS`.
### Single App
```jsonc
// wrangler.jsonc
{
"flagship": {
"binding": "FLAGS",
"app_id": "<APP_ID>"
}
}
```
```toml
# wrangler.toml
[flagship]
binding = "FLAGS"
app_id = "<APP_ID>"
```
### Multiple Apps
```jsonc
// wrangler.jsonc
{
"flagship": [
{
"binding": "FLAGS",
"app_id": "<APP_ID_1>"
},
{
"binding": "EXPERIMENT_FLAGS",
"app_id": "<APP_ID_2>"
}
]
}
```
```toml
# wrangler.toml
[[flagship]]
binding = "FLAGS"
app_id = "<APP_ID_1>"
[[flagship]]
binding = "EXPERIMENT_FLAGS"
app_id = "<APP_ID_2>"
```
### Generate Types
After adding the binding, generate TypeScript types:
```bash
npx wrangler types
```
This creates the `Env` interface with each binding typed as `Flagship`:
```typescript
interface Env {
FLAGS: Flagship;
EXPERIMENT_FLAGS: Flagship; // if multiple
}
```
The `Flagship` type comes from `@cloudflare/workers-types`.
---
## OpenFeature SDK Installation
### Server-Side (Workers, Node.js)
```bash
npm i @cloudflare/flagship @openfeature/server-sdk
```
### Browser
```bash
npm i @cloudflare/flagship @openfeature/web-sdk
```
---
## SDK Provider Setup
### Server Provider — With Binding (Workers)
Recommended approach inside Workers. No HTTP overhead, auth handled automatically.
```typescript
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();
// ... evaluate flags
},
};
```
### Server Provider — With App ID (Node.js)
For non-Worker runtimes. Requires an API token with Flagship read permissions.
```typescript
import { OpenFeature } from "@openfeature/server-sdk";
import { FlagshipServerProvider } from "@cloudflare/flagship";
await OpenFeature.setProviderAndWait(
new FlagshipServerProvider({
appId: "<APP_ID>",
accountId: "<ACCOUNT_ID>",
authToken: "<API_TOKEN>",
}),
);
const client = OpenFeature.getClient();
```
### Client Provider (Browser)
Pre-fetches flags on init, then evaluates synchronously. Only `prefetchFlags` are available.
```typescript
import { OpenFeature } from "@openfeature/web-sdk";
import { FlagshipClientProvider } from "@cloudflare/flagship";
await OpenFeature.setProviderAndWait(
new FlagshipClientProvider({
appId: "<APP_ID>",
accountId: "<ACCOUNT_ID>",
authToken: "<API_TOKEN>",
prefetchFlags: ["promo-banner", "dark-mode", "max-uploads"],
}),
);
await OpenFeature.setContext({ targetingKey: "user-42", plan: "enterprise" });
const client = OpenFeature.getClient();
```
### Provider Options Reference
**FlagshipServerProvider:**
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `binding` | `Flagship` | No | Binding from `env.FLAGS`. Use inside Workers. |
| `appId` | string | No | App ID from dashboard. Required without binding. |
| `accountId` | string | No | Cloudflare account ID. Required without binding. |
| `authToken` | string | No | API token with Flagship read permissions. Required without binding. |
Provide either `binding` or all three of `appId` + `accountId` + `authToken`.
**FlagshipClientProvider:**
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `appId` | string | Yes | App ID from dashboard |
| `accountId` | string | Yes | Cloudflare account ID |
| `authToken` | string | Yes | API token with Flagship read permissions |
| `prefetchFlags` | string[] | Yes | Flag keys to prefetch. Unlisted flags return `FLAG_NOT_FOUND`. |
---
## REST API Authentication
For managing flags via the REST API (create, update, delete), set these environment variables:
| Variable | Description |
|----------|-------------|
| `CLOUDFLARE_ACCOUNT_ID` | Your Cloudflare account ID |
| `CLOUDFLARE_API_TOKEN` | API token with Flagship permissions |
| `FLAGSHIP_APP_ID` | Target app UUID (from dashboard under **Compute > Flagship**, or `GET /apps`) |
Base URL: `https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship`
```bash
curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/flagship/apps" | jq .
```
App IDs are shown in the Cloudflare dashboard under **Compute > Flagship**.
---
## Local Development
Flagship bindings work in local dev with `wrangler dev`. Flag evaluation uses the live Flagship configuration — there is no local flag store. Ensure the `app_id` in your Wrangler config points to a valid app.
```bash
npx wrangler dev
```
@@ -0,0 +1,178 @@
# Flagship Gotchas & Troubleshooting
## Common Errors
### Flag Always Returns Default Value
**Cause:** Flag is disabled (`enabled: false`), or no targeting rules match, or evaluation context is missing expected attributes.
**Solution:** Check these in order:
1. Is the flag enabled? (`"enabled": true`)
2. Do your targeting rules match the context you're passing?
3. Are you passing the right attributes in the evaluation context?
```typescript
// ❌ BAD — no context, rules can't match
const val = await env.FLAGS.getBooleanValue("my-flag", false);
// ✅ GOOD — pass context attributes that rules reference
const val = await env.FLAGS.getBooleanValue("my-flag", false, {
userId: "user-42",
plan: "enterprise",
});
```
### TYPE_MISMATCH Error in Details
**Cause:** Calling a typed method on a flag with a different type (e.g., `getBooleanValue` on a string flag).
**Solution:** Use the method matching the flag's variation type.
```typescript
// ❌ BAD — flag "checkout-flow" has string variations
const val = await env.FLAGS.getBooleanValue("checkout-flow", false);
// ✅ GOOD
const val = await env.FLAGS.getStringValue("checkout-flow", "original");
```
### 409 Conflict on Flag Creation
**Cause:** A flag with that key already exists in the app.
**Solution:** Use a different key, or GET + PUT to update the existing flag.
### Inconsistent Rollout Results
**Cause:** `targetingKey` (or the configured bucketing attribute) is missing from the evaluation context, causing random bucketing on each request.
**Solution:** Always pass a stable identifier:
```typescript
// ❌ BAD — no targetingKey, rollout is random per request
const val = await env.FLAGS.getBooleanValue("gradual-rollout", false);
// ✅ GOOD — stable userId for consistent bucketing
const val = await env.FLAGS.getBooleanValue("gradual-rollout", false, {
userId: sessionUserId,
});
```
### Update Overwrites Entire Flag
**Cause:** PUT replaces the full `FlagDefinition`. Sending only changed fields deletes the rest.
**Solution:** Always read-modify-write:
```bash
# ❌ BAD — overwrites the entire flag, losing rules/variations
curl -X PUT -d '{"enabled": true}' ...
# ✅ GOOD — GET first, modify, PUT back
FLAG=$(curl -s -H "Authorization: Bearer $TOKEN" "$URL/flags/my-flag" | jq '.result')
UPDATED=$(echo "$FLAG" | jq '.enabled = true')
echo "$UPDATED" | curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @- "$URL/flags/my-flag"
```
### Reading REST Envelope Fields
**Cause:** Management endpoints use Cloudflare v4 envelopes, not raw payloads.
**Solution:** Read `.result` for successful payloads, `.result_info.cursor` for pagination, and `.errors[].message` for errors.
```bash
jq '.result'
jq '.result_info.cursor'
jq '.errors[].message'
```
### Mixing CamelCase and Snake Case in REST Responses
**Cause:** Management API responses are public API JSON and use snake_case. Evaluation responses use OpenFeature-style camelCase.
**Solution:** For management endpoints use `default_variation`, `serve_variation`, `updated_at`, `updated_by`, and changelog `flag_key`. For `/evaluate`, use `flagKey`, `variant`, and `reason`.
### FLAG_NOT_FOUND in Client Provider
**Cause:** Flag key not included in `prefetchFlags` array.
**Solution:** Add the flag key to `prefetchFlags` when initializing `FlagshipClientProvider`.
### Client Provider Token Exposure
**Cause:** The `authToken` passed to `FlagshipClientProvider` is visible in the browser. It can evaluate flags across all apps in the account.
**Solution:** Use a token with minimal permissions (Flagship Evaluate only). Never use a token with write/management permissions in the browser.
---
## Limits
| Limit | Value | Notes |
|-------|-------|-------|
| Flag key length | 1-64 chars | Alphanumeric, hyphens, underscores only |
| Flag key pattern | `/^[a-zA-Z0-9_-]+$/` | — |
| Variation value size | 10KB max | Per variation, serialized |
| Variation name length | 64 chars max | Alphanumeric, hyphens, underscores |
| Description length | 512 chars max | Nullable |
| App name length | 1-64 chars | Alphanumeric, hyphens, underscores |
| Logical nesting depth | 6 levels | AND/OR conditions |
| Mutation rate limit | 60 / 60s | Per account:app |
| Read rate limit | 600 / 60s | Per account:app |
| Rollout percentage | 0-100 | Integer |
| Rule priorities | Unique integers >= 1 | Lower = evaluated first |
---
## Anti-Patterns
### Evaluating Flags in a Tight Loop
Flag evaluation via the binding is fast but not free. Avoid evaluating the same flag repeatedly in a loop — evaluate once and reuse the result.
```typescript
// ❌ BAD
for (const item of items) {
const enabled = await env.FLAGS.getBooleanValue("my-flag", false, ctx);
// ...
}
// ✅ GOOD
const enabled = await env.FLAGS.getBooleanValue("my-flag", false, ctx);
for (const item of items) {
// use `enabled`
}
```
### Using the SDK Inside Workers When Binding Is Available
The binding avoids HTTP overhead entirely. Only use the SDK inside Workers when you specifically need OpenFeature vendor-neutrality.
```typescript
// ❌ Unnecessary HTTP overhead inside a Worker
const provider = new FlagshipServerProvider({
appId: "...", accountId: "...", authToken: "...",
});
// ✅ Use the binding directly, or pass it to the SDK
const provider = new FlagshipServerProvider({ binding: env.FLAGS });
```
### Partial PUT Updates
The flag update API (PUT) requires the complete `FlagDefinition`. Sending only changed fields silently drops everything else. Always GET first, then modify and PUT back the full object.
### Stale Flag Cleanup
Flags that are disabled and no longer referenced in code should be deleted. Stale flags clutter the dashboard and make it harder to understand which flags are active. Follow the safe deletion workflow in `patterns.md`.
---
## Propagation Behavior
Flag changes propagate globally within seconds. During the brief propagation window, some regions may serve the previous value. After propagation completes, all evaluations return the updated value.
- No Worker redeployment needed for flag changes.
- If the dashboard is temporarily unavailable, evaluation continues using the last propagated configuration.
- Flag changes made via the REST API and dashboard are equivalent — both trigger propagation.
@@ -0,0 +1,469 @@
# Flagship Patterns & Best Practices
## Evaluating Flags in Workers (Binding)
### Simple Boolean Toggle
```typescript
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
```typescript
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
```typescript
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
```typescript
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)
```typescript
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:
```typescript
// ❌ 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
```bash
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
```bash
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
```bash
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
```bash
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)
```bash
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 `result_info.cursor` is non-null, fetch the next page:
```bash
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=<cursor>" | jq .
```
### Update a Flag (Full Replace)
Updates use PUT with the full `FlagDefinition`. Always GET first, modify, then PUT back.
```bash
# 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 '.result')
# 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`:
```bash
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 '.result')
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.
```bash
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 '.result')
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.
```bash
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 '.result')
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):
```bash
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 '.result')
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
```bash
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 '.result')
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
```bash
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 '.result')
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:
```bash
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 '.result')
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
```bash
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
```json
{
"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:
```json
{
"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:
```json
{
"priority": 1,
"conditions": [
{ "attribute": "targetingKey", "operator": "not_equals", "value": "" }
],
"serve_variation": "on",
"rollout": {
"percentage": 10,
"attribute": "targetingKey"
}
}
```
### A/B/n (Multi-Variant) Testing
To split traffic across N variants, create one rule per variant with **cumulative** rollout percentages. Flagship evaluates rules in priority order. If a rule's conditions match but the user misses that rule's rollout percentage, evaluation continues to the next rule. Use the same stable rollout attribute on every rule so each user is compared against the same bucket as the thresholds increase.
The example uses `conditions: []` because the rules are intended to match every context. For sticky user assignment, callers must still pass the configured bucketing attribute (`targetingKey` here); otherwise Flagship uses a random bucket per request.
For example, to split traffic 30% / 40% / 30% across variants A, B, and C:
| Variant | Share | Cumulative threshold |
|---------|-------|----------------------|
| A | 30% | 30 |
| B | 40% | 70 |
| C | 30% | 100 |
```json
"rules": [
{
"priority": 1,
"conditions": [],
"serve_variation": "variant-a",
"rollout": { "percentage": 30, "attribute": "targetingKey" }
},
{
"priority": 2,
"conditions": [],
"serve_variation": "variant-b",
"rollout": { "percentage": 70, "attribute": "targetingKey" }
},
{
"priority": 3,
"conditions": [],
"serve_variation": "variant-c",
"rollout": { "percentage": 100, "attribute": "targetingKey" }
}
]
```
Key points:
- Rules are evaluated lowest-priority-number first. A user who falls into rule 1's 0-30% bucket gets `variant-a` and is not evaluated further.
- Rule 2's 70% threshold covers the next 40% of users (31-70%).
- Rule 3's 100% threshold catches the remaining 30% (71-100%).
- Always set the last rule to `100` so every context with the bucketing attribute is assigned a variant.
- For sticky A/B/n assignment, pass a stable `targetingKey` or configured bucketing attribute. Without it, rollout assignment is random per request, which can be useful for request-level sampling but is usually wrong for user experiments.
- A percentage rollout match reports reason `SPLIT` in evaluation details.
### Progressive Rollout Workflow
1. Create flag with 5% rollout, enable it
2. Monitor metrics
3. Increase to 25% → 50% → 100% by updating the `rollout.percentage`
4. Once at 100%, remove the rule and set `default_variation` to the winning variation
5. Eventually remove the flag and the code branch
---
## Safe Deletion Workflow
1. **Disable** the flag first (`enabled: false`) — confirms nothing depends on it being active
2. **Monitor** for unexpected behavior
3. **Remove** flag evaluation code from your application
4. **Deploy** the code change
5. **Delete** the flag via API