@cloudflare/flagship
v0.6.0
Published
OpenFeature provider for Flagship feature flags
Readme
@cloudflare/flagship
OpenFeature-compliant provider SDK for Flagship, Cloudflare's globally distributed, low-latency feature flag platform.
Server-side (Node.js, Cloudflare Workers) and client-side (browser) support via isolated sub-path exports. Tree-shakeable — importing @cloudflare/flagship/server never loads @openfeature/web-sdk and vice versa.
Install
Server-side (Node.js, Cloudflare Workers):
npm install @cloudflare/flagship @openfeature/server-sdkClient-side (browser):
npm install @cloudflare/flagship @openfeature/web-sdkQuick start — server
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagshipServerProvider } from '@cloudflare/flagship/server';
await OpenFeature.setProviderAndWait(
new FlagshipServerProvider({ appId: 'your-app-id', accountId: 'your-account-id', authToken: 'your-token' }),
);
const client = OpenFeature.getClient();
const enabled = await client.getBooleanValue('dark-mode', false, { userId: 'user-123' });Quick start — Cloudflare Workers
The recommended approach for Cloudflare Workers. Uses a wrangler binding — no HTTP overhead, no auth tokens needed.
Configure in wrangler.json:
{
"flagship": [{ "binding": "FLAGS", "app_id": "<your-app-id>" }]
}import { OpenFeature } from '@openfeature/server-sdk';
import { FlagshipServerProvider } from '@cloudflare/flagship/server';
import type { FlagshipBinding } from '@cloudflare/flagship/server';
export default {
async fetch(request: Request, env: { FLAGS: FlagshipBinding }): Promise<Response> {
await OpenFeature.setProviderAndWait(new FlagshipServerProvider({ binding: env.FLAGS }));
const client = OpenFeature.getClient();
const darkMode = await client.getBooleanValue('dark-mode', false, {
targetingKey: new URL(request.url).searchParams.get('userId') ?? 'anonymous',
});
return Response.json({ darkMode });
},
};Quick start — HTTP
For Workers without a Flagship binding, or non-Workers server environments:
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagshipServerProvider } from '@cloudflare/flagship/server';
let initialized = false;
export default {
async fetch(request: Request): Promise<Response> {
if (!initialized) {
await OpenFeature.setProviderAndWait(
new FlagshipServerProvider({ appId: 'your-app-id', accountId: 'your-account-id', authToken: 'your-token' }),
);
initialized = true;
}
const client = OpenFeature.getClient();
const darkMode = await client.getBooleanValue('dark-mode', false, {
userId: new URL(request.url).searchParams.get('userId') ?? 'anonymous',
});
return Response.json({ darkMode });
},
};Structured evaluation context
Binding and HTTP modes support nested objects, arrays, and null in evaluation context. Dates are recursively converted to ISO-8601 strings.
const checkout = await client.getStringValue('checkout-flow', 'control', {
targetingKey: 'user-123',
profile: { account: { plan: 'enterprise' } },
tags: ['beta', 'internal'],
});Primitive-only HTTP context keeps using GET query parameters. Structured context, including null, uses the JSON POST endpoint; in browsers this adds a CORS preflight request. Only strings, numbers, booleans, null, dates, arrays, and plain objects are supported. Anything else, including cyclic values, resolves with INVALID_CONTEXT without making a binding or HTTP call.
Caching
The server provider can cache evaluations to avoid a network round-trip (HTTP mode) or binding call (binding mode) for repeated flag/context pairs. Caching is off by default and enabled by setting cacheTtl:
new FlagshipServerProvider({
appId: 'your-app-id',
accountId: 'your-account-id',
cacheTtl: 30_000, // ms — enables caching; values may be up to this stale
cacheMaxSize: 1000, // max cached entries, LRU-evicted (default: 1000)
});Each entry is keyed by flag key, type, and the full evaluation context, so distinct contexts never share a value. Cache hits resolve with reason: 'CACHED'. Disabled flags and errors are never cached. Because freshness is TTL-based, a flag change in Flagship takes effect after the entry expires.
Local evaluation
Server providers can download flag definitions once and evaluate flags in-process with no network call per evaluation. Definitions refresh lazily in the background (stale-while-revalidate on evaluate).
await OpenFeature.setProviderAndWait(
new FlagshipServerProvider({
appId: 'your-app-id',
accountId: 'your-account-id', // required — used as the rollout hash seed
authToken: 'your-read-token', // needs app **read** permission, not evaluate
localEvaluation: true,
refreshInterval: 30_000, // ms between background refreshes (default 30s)
}),
);Notes:
- Incompatible with
bindingmode and withcacheTtl(there is nothing to cache). accountIdis always required in local mode, including whenendpointis used.- With
endpoint, the URL path must end in/evaluateso/definitionscan be derived. - Local evaluations do not appear in server-side analytics.
- See
examples/server-local.ts.
Quick start — browser
import { OpenFeature } from '@openfeature/web-sdk';
import { FlagshipClientProvider } from '@cloudflare/flagship/web';
await OpenFeature.setProviderAndWait(
new FlagshipClientProvider({
appId: 'your-app-id',
accountId: 'your-account-id',
authToken: 'your-token',
prefetchFlags: ['dark-mode', 'welcome-message'],
}),
);
await OpenFeature.setContext({ userId: 'user-123', plan: 'premium' });
const client = OpenFeature.getClient();
const darkMode = client.getBooleanValue('dark-mode', false);Features
| Feature | Description |
| --------------------- | ------------------------------------------------------------------------------ |
| OpenFeature compliant | Implements the CNCF OpenFeature specification |
| Workers binding | Native wrangler binding support — zero HTTP overhead, no auth tokens |
| Server + client | Async per-request (server) and sync cache-based (browser) providers |
| Server providers | FlagshipServerProvider works via HTTP, local evaluation, or wrangler binding |
| Local evaluation | In-process evaluation from downloaded definitions (localEvaluation: true) |
| All flag types | Boolean, string, number, and object (JSON) |
| Authentication | authToken option adds Authorization: Bearer to every request (HTTP only) |
| Logging | logging option surfaces fetch errors and cache misses (off by default) |
| Response caching | Opt-in per-context TTL + LRU cache via cacheTtl (off by default) |
| Retries + timeouts | Configurable retry logic with AbortController-based timeouts (HTTP only) |
| Custom transport | Inject fetch per client or per call — no global mutation (HTTP only) |
| Cancellation | Caller AbortSignal aborts the in-flight request, never retried (HTTP only) |
| Hooks | Built-in LoggingHook and TelemetryHook |
| Tree-shakeable | Server and client bundles are fully isolated |
| TypeScript | Strict types throughout |
Packages
| Export | Description | Peer dependency |
| ----------------------------- | -------------------------------- | ------------------------- |
| @cloudflare/flagship | Core client, types, errors | None |
| @cloudflare/flagship/server | FlagshipServerProvider + hooks | @openfeature/server-sdk |
| @cloudflare/flagship/web | FlagshipClientProvider | @openfeature/web-sdk |
Documentation
Development
pnpm install # install dependencies
pnpm run dev # watch mode
pnpm run test # run tests
pnpm run build # build for distributionContributing
Contributions are welcome. Please open an issue first to discuss what you'd like to change. See the repository for more details.
