Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions apps/docs/content/docs/api-reference/frameworks/next.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,24 @@ This call is a shorthand for calling `evaluate` and `serialize` manually.

### `evaluate`

**Description**: Evaluates multiple feature flags and returns their values.
**Description**: Evaluates multiple feature flags in a single call and returns their values.

| Parameter | Type | Description |
| --------- | ------------ | ---------------------------------------- |
| `flags` | `function[]` | An array of flags declared using `flag`. |
This is the recommended way to evaluate multiple feature flags at once. Prefer it over `Promise.all([flagA(), flagB()])`: `evaluate` pre-reads headers, cookies, and overrides once for the whole batch and lets adapters that implement [`bulkDecide`](/providers/custom-adapters#bulk-evaluation) resolve a group through a single call. This reduces the number of parallel promises and leaves less room for the work to be interrupted by other microtasks. See [Bulk evaluation](/frameworks/next/bulk-evaluation) for details.

| Parameter | Type | Description |
| -------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------- |
| `flags` | `Flag[]` \| `Record<string, Flag>` | An array of flags (positional results) or an object whose values are flags (keyed results). |
| `request` (Optional) | `IncomingMessage` \| `NextRequest` \| `Request` | Required outside App Router — pass it in Pages Router or routing middleware. |

```ts
import { evaluate } from 'flags/next';
const values = await evaluate(precomputeFlags);
import { flagA, flagB } from './flags';

// Array form — positional results
const [a, b] = await evaluate([flagA, flagB]);

// Object form — keyed results
const { a, b } = await evaluate({ a: flagA, b: flagB });
```

### `serialize`
Expand Down
118 changes: 118 additions & 0 deletions apps/docs/content/docs/frameworks/next/bulk-evaluation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: Bulk Evaluation
description: Evaluate multiple feature flags at once with evaluate().
---


`evaluate` (from `flags/next`) resolves multiple feature flags in a single call.
Use it instead of awaiting flags one at a time or resolving them with
`Promise.all`, both of which add avoidable latency or overhead. To evaluate a
**single** flag, keep calling it directly with `await myFlag()`.

## Why not await flags one by one or with `Promise.all`?

Awaiting each flag in turn blocks on every flag before starting the next one, so
the flags resolve sequentially. Total latency becomes the sum of every flag's
evaluation time instead of the slowest single flag.

```ts title="example.ts"
import { flagA, flagB } from './flags';

// avoid: each await blocks the next, so the flags resolve sequentially
const a = await flagA();
const b = await flagB();
```

`Promise.all` removes the sequential wait by starting every flag at once, but it
evaluates each flag in isolation.

```ts title="example.ts"
import { flagA, flagB } from './flags';

// avoid: resolves in parallel, but can't share work across the flags
const [a, b] = await Promise.all([flagA(), flagB()]);
```

Because each flag runs on its own, `Promise.all` can't reuse work across the
batch. Every flag reads headers, cookies, and overrides again, and adapters
can't resolve a group of flags through a single call. It also creates one
promise per flag, with each flag spawning further internal promises as it
evaluates, which adds microtask queue overhead and leaves more room for the work
to be interrupted by other microtasks.

## Use `evaluate` instead

`evaluate` resolves a set of flags in a single call. It pre-reads headers,
cookies, and overrides once for the whole batch and lets adapters resolve a
group through one call, so it shares work across evaluations and reduces the
number of parallel promises the runtime has to manage.

```ts title="example.ts"
import { evaluate } from 'flags/next';
import { flagA, flagB } from './flags';

// prefer: shares work across the batch
const [a, b] = await evaluate([flagA, flagB]);
```

`evaluate` accepts either an **array** of flags, returning positional results,
or an **object** whose values are flags, returning keyed results.

```ts title="example.ts"
// array form — positional results
const [a, b] = await evaluate([flagA, flagB]);

// object form — keyed results
const { a, b } = await evaluate({ a: flagA, b: flagB });
```

## Evaluate outside the App Router

Outside the App Router, in Pages Router (`getServerSideProps`, API routes) or in
routing middleware, pass the request as the second argument so `evaluate` can
read headers and cookies:

```ts title="middleware.ts"
const [a, b] = await evaluate([flagA, flagB], request);
```

## Evaluation context

`evaluate` accepts only flags and an optional request. It does not take an
evaluation context or entities argument. Each flag resolves its own
[evaluation context](/frameworks/next/evaluation-context) from the `identify`
function declared on the flag, and `evaluate` calls that `identify` for you,
sharing the result across the batch.

To control the context for a flag evaluated through `evaluate`, set it in that
flag's `identify` function, which can read the request's headers and cookies and
return whatever entities you need. Passing entities directly is only supported
when calling a single flag with `await myFlag.run({ identify: entities })`,
which bypasses `identify` and uses the entities you provide.

## Evaluate vs. precomputed values

`evaluate` always evaluates flags **dynamically** at request time. It calls each
flag's adapter (or `decide`), just as calling the flag directly does.

It is **not** the way to read the values of [precomputed](/frameworks/next/precompute)
flags. When flags were precomputed in the proxy and encoded into a `code`, read
their values without re-evaluating using
[`getPrecomputed`](/api-reference/frameworks/next#getprecomputed) (or by calling
the flag with the code, `await myFlag(code, flagGroup)`).

## Adapters can batch evaluation

Aside from the Flags SDK itself getting faster, adapters can implement the
optional [`bulkDecide`](/providers/custom-adapters#bulk-evaluation) hook. When an
adapter implements it, `evaluate` calls `bulkDecide` once per group of flags that
share an adapter and `identify` source, instead of calling `decide` per flag, so
the underlying provider can share work across evaluations too.

The [Vercel adapter](/providers/vercel) (`@flags-sdk/vercel`) implements
`bulkDecide`, with roughly a 10x reduction in evaluation time when resolving
hundreds of flags in parallel.

<LearnMore href="/providers/custom-adapters#bulk-evaluation" icon="arrow">
Implement `bulkDecide` in a custom adapter
</LearnMore>
1 change: 1 addition & 0 deletions apps/docs/content/docs/frameworks/next/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"---Concepts---",
"evaluation-context",
"dedupe",
"bulk-evaluation",
"precompute",
"---Guides---",
"...guides",
Expand Down
65 changes: 53 additions & 12 deletions apps/docs/content/docs/providers/custom-adapters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ title: Custom Adapters
description: Integrate any feature flag provider with the Flags SDK using a custom adapter.
---


Integrate any feature flag provider with the Flags SDK
using an adapter. We publish adapters for the most [common providers](/docs/adapters/supported-providers), but
it is also possible to write a custom adapter in case we don't list
your provider or in case you have an in-house solution for feature
flags.

<CopyPrompt text="Create a custom Flags SDK adapter for my feature flag provider. Implement an adapter factory that initializes or accepts my provider client, define `origin` and `decide` behavior, expose a default adapter when appropriate, use the adapter from typed flag declarations, preserve Edge Runtime compatibility where possible, and run the relevant tests or build when finished.">
Create a custom Flags SDK adapter for my feature flag provider. Implement an adapter factory that initializes or accepts my provider client, define `origin` and `decide` behavior, expose a default adapter when appropriate, use the adapter from typed flag declarations, preserve Edge Runtime compatibility where possible, and run the relevant tests or build when finished.
<CopyPrompt text="Create a custom Flags SDK adapter for my feature flag provider. Implement an adapter factory that initializes or accepts my provider client, define `origin` and `decide` behavior, optionally implement `bulkDecide` (and set `adapterId`) for batch evaluation, expose a default adapter when appropriate, use the adapter from typed flag declarations, preserve Edge Runtime compatibility where possible, and run the relevant tests or build when finished.">
Create a custom Flags SDK adapter for my feature flag provider. Implement an
adapter factory that initializes or accepts my provider client, define
`origin` and `decide` behavior, optionally implement `bulkDecide` (and set
`adapterId`) for batch evaluation, expose a default adapter when appropriate,
use the adapter from typed flag declarations, preserve Edge Runtime
compatibility where possible, and run the relevant tests or build when
finished.
</CopyPrompt>

Adapters conceptually replace the `decide` and `origin` parts of a flag declaration.
Expand All @@ -21,8 +26,8 @@ Adapters conceptually replace the `decide` and `origin` parts of a flag declarat
Creating custom adapters is possible by creating an adapter factory:

```ts title="example-adapter.ts"
import type { Adapter } from 'flags';
import { createClient, EdgeConfigClient } from '@vercel/edge-config';
import type { Adapter } from "flags";
import { createClient, EdgeConfigClient } from "@vercel/edge-config";

/**
* A factory function for your adapter
Expand Down Expand Up @@ -52,19 +57,55 @@ export function createExampleAdapter(/* options */) {
This allows passing the provider in the flag declaration.

```tsx title="flags.tsx#next"
import { flag } from 'flags/next';
import { createExampleAdapter } from './example-adapter';
import { flag } from "flags/next";
import { createExampleAdapter } from "./example-adapter";

// create an instance of the adapter
const exampleAdapter = createExampleAdapter();

export const exampleFlag = flag({
key: 'example-flag',
key: "example-flag",
// use the adapter for many feature flags
adapter: exampleAdapter,
});
```

## Bulk evaluation

Adapters can implement an optional `bulkDecide` hook to share work when many flags are
evaluated together with [`evaluate`](/frameworks/next/bulk-evaluation). When `bulkDecide` is
implemented and the adapter sets an `adapterId`, `evaluate` calls `bulkDecide` once for each
group of flags that share this adapter and the same `identify` source — instead of calling
`decide` per flag. This lets the provider, for example, resolve many flags through a single
network request.

```ts title="example-adapter.ts"
return {
// Required for `bulkDecide` to be used by `evaluate`.
adapterId: "example",
origin(key) {
return `https://example.com/flags/${key}`;
},
async decide({ key }): Promise<ValueType> {
return false as ValueType;
},
// Called by `evaluate` for a batch of flags sharing this adapter and identify.
async bulkDecide({ flags, entities, headers, cookies }) {
// `flags` is `{ key: string; defaultValue?: unknown }[]`.
// Resolve them however your provider allows — ideally in a single call.
return Object.fromEntries(
flags.map(({ key }) => [key, false as ValueType])
);
},
};
```

`bulkDecide` must return a record keyed by flag key:

- Missing keys or a `value` of `undefined` fall back to that flag's `defaultValue`.
- Throwing falls back to `defaultValue` per flag (and rejects for flags without a `defaultValue`).
- A flag declared with an inline `decide` takes precedence and is excluded from bulk evaluation.

## Example

Below is an example of an Flags SDK adapter reading Edge Config.
Expand All @@ -84,11 +125,11 @@ Usage with a default adapter, where we can import a fully configured{" "}
`exampleAdapter`.

```tsx title="flags.tsx#next"
import { flag } from 'flags/next';
import { exampleAdapter } from './example-adapter';
import { flag } from "flags/next";
import { exampleAdapter } from "./example-adapter";

export const exampleFlag = flag({
key: 'example-flag',
key: "example-flag",
// use the adapter for many feature flags
adapter: exampleAdapter,
});
Expand All @@ -115,7 +156,7 @@ export function edgeConfigAdapter<ValueType, EntitiesType>(): Adapter<
// Initialized lazily to avoid warning when it is not actually used and env vars are missing.
if (!defaultEdgeConfigAdapter) {
if (!process.env.EDGE_CONFIG) {
throw new Error('Edge Config Adapter: Missing EDGE_CONFIG env var');
throw new Error("Edge Config Adapter: Missing EDGE_CONFIG env var");
}

defaultEdgeConfigAdapter = createEdgeConfigAdapter(process.env.EDGE_CONFIG);
Expand Down
34 changes: 34 additions & 0 deletions skills/flags-sdk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,40 @@ const identify = dedupe(({ cookies }) => {

Note: `dedupe` is not available in Pages Router.

## Bulk evaluation

To evaluate **multiple** flags at once, call `evaluate()` (from `flags/next`) instead of awaiting flags one at a time or using `Promise.all()`. To evaluate a **single** flag, just call it: `await myFlag()`.

```ts
import { evaluate } from 'flags/next';
import { flagA, flagB } from '../flags';

// avoid: each await blocks the next, so the flags resolve sequentially
const a = await flagA();
const b = await flagB();

// avoid: parallel, but each flag is evaluated in isolation
const [a, b] = await Promise.all([flagA(), flagB()]);

// prefer: shares work across the batch
const [a, b] = await evaluate([flagA, flagB]);
```

`evaluate()` is faster than both approaches. Awaiting flags one at a time makes total latency the sum of every flag's evaluation instead of the slowest single flag, while `Promise.all()` runs them in parallel but evaluates each in isolation. `evaluate()` pre-reads headers, cookies, and overrides once for the whole batch and lets adapters resolve a group in a single call, which reduces the number of parallel promises the runtime manages and leaves less room for the async work to be interrupted by other microtasks.

It accepts either an **array** (positional results) or an **object** (keyed results):

```ts
const [a, b] = await evaluate([flagA, flagB]);
const { a, b } = await evaluate({ a: flagA, b: flagB });
```

Outside App Router (Pages Router `getServerSideProps`/API routes, or routing middleware), pass the request as the second argument: `await evaluate([flagA, flagB], request)`.

`evaluate()` always evaluates flags at request time. It is not for reading [precomputed](#precompute-pattern) (static) values — for those, use `getPrecomputed` (or call the flag with the code, `await myFlag(code, flagGroup)`).

Adapters can opt into batching by implementing the optional `bulkDecide` hook. The Vercel adapter (`@flags-sdk/vercel`) implements it — roughly a 10x reduction in evaluation time when resolving hundreds of flags. See [references/providers.md — Custom Adapters](references/providers.md#custom-adapters) for implementing `bulkDecide`, and [references/api.md — `evaluate`](references/api.md#evaluate) for the full signature.

## Flags Explorer setup

### Next.js (App Router)
Expand Down
14 changes: 13 additions & 1 deletion skills/flags-sdk/references/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,19 @@ Evaluate multiple flags, return encoded string.

### `evaluate`

Evaluate multiple flags, return their values as array.
Evaluate multiple flags in a single call. Prefer this over `Promise.all([flagA(), flagB()])` — it pre-reads headers, cookies, and overrides once for the whole batch and lets adapters resolve a group through one `bulkDecide` call, reducing parallel promises and microtask overhead.

| Parameter | Type | Description |
| ------------------- | ----------------------------------- | ------------------------------------------------------------------- |
| `flags` | `Flag[] \| Record<string, Flag>` | Array (positional results) or object (keyed results) of flags |
| `request` (Optional)| `IncomingMessage \| Request` | Required outside App Router (Pages Router or routing middleware) |

```ts
import { evaluate } from 'flags/next';

const [a, b] = await evaluate([flagA, flagB]); // positional
const { a, b } = await evaluate({ a: flagA, b: flagB }); // keyed
```

### `serialize`

Expand Down
30 changes: 30 additions & 0 deletions skills/flags-sdk/references/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,36 @@ export function createMyAdapter(/* options */) {
}
```

### Bulk evaluation (`bulkDecide`)

Adapters can implement an optional `bulkDecide` hook. When set (and the adapter has an `adapterId`), `evaluate()` calls it once for every group of flags that share this adapter and the same `identify` source — instead of calling `decide` per flag. This lets the provider share work across evaluations (e.g. a single network request for many flags).

```ts
return {
adapterId: 'my-provider', // required for bulkDecide to be used
origin(key) {
return `https://my-provider.com/flags/${key}`;
},
async decide({ key }): Promise<ValueType> {
return false as ValueType;
},
// Called by evaluate() for a batch of flags sharing this adapter + identify
async bulkDecide({ flags, entities, headers, cookies }) {
// flags: { key: string; defaultValue?: unknown }[]
// Return a record keyed by flag key.
return Object.fromEntries(
flags.map(({ key }) => [key, false as ValueType]),
);
},
};
```

Contract:

- Return `Record<flagKey, value>`. Missing keys or `value: undefined` fall back to each flag's `defaultValue`.
- Throwing falls back to `defaultValue` per flag (and rejects for flags without a `defaultValue`).
- A flag with an inline `decide` takes precedence and is excluded from bulk evaluation.

### Default adapter pattern

Expose a lazily-initialized default for simpler usage:
Expand Down
Loading