Skip to content
Open
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
25 changes: 25 additions & 0 deletions .changeset/tanstack-start-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'flags': minor
---

Add TanStack Start support via a new `flags/tanstack-start` entrypoint

The Flags SDK now ships a first-class adapter for [TanStack Start](https://tanstack.com/start/latest), following the same patterns as the Next.js and SvelteKit entrypoints.

```ts
// src/flags.ts
import { flag } from 'flags/tanstack-start';

export const exampleFlag = flag<boolean>({
key: 'example-flag',
decide: () => true,
});
```

Flags can be evaluated with no arguments inside a route loader, server function,
or server route — the request is resolved automatically through TanStack Start's
`getRequest()`. You may also pass a `Request` explicitly (e.g. `flag(request)`).

The entrypoint exports `flag`, `getProviderData`, `createFlagsDiscoveryEndpoint`,
`precompute`, `generatePermutations`, and the `encrypt*`/`decrypt*` helpers, plus
support for Vercel Toolbar overrides via the `vercel-flag-overrides` cookie.
124 changes: 124 additions & 0 deletions apps/docs/content/docs/api-reference/frameworks/tanstack-start.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
title: 'flags/tanstack-start'
description: APIs for working with feature flags in TanStack Start.
---

### `flag`

**Description**: Declares a feature flag.

A feature flag declared this way will automatically respect overrides set by Vercel Toolbar and integrate with Runtime Logs, Web Analytics, and more.

| Parameter | Type | Description |
| ------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------- |
| `key` | `string` | Key of the feature flag. |
| `decide` | `function` | Resolves the value of the feature flag. |
| `description` (Optional) | `string` | Description of the feature flag. |
| `origin` (Optional) | `string` | The URL where this feature flag can be managed. |
| `options` (Optional) | `{ label?: string, value: any }[]` | Possible values a feature flag can resolve to, which are displayed in Vercel Toolbar. |
| `identify` (Optional) | `function` | Provide an evaluation context which will be passed to `decide`. |
| `defaultValue` (Optional)| `any` | Returned when `decide` (or the adapter) resolves to `undefined`. |
| `adapter` (Optional) | `Adapter` | A provider adapter implementing `decide` and `origin`. |

The `key`, `description`, `origin`, and `options` appear in Vercel Toolbar.

```ts title="src/flags.ts"
import { flag } from 'flags/tanstack-start';

export const showSummerSale = flag<boolean>({
key: 'summer-sale',
async decide() {
return false;
},
origin: 'https://example.com/flags/summer-sale/',
description: 'Show Summer Holiday Sale Banner, 20% off',
options: [
// options are not necessary for boolean flags, but we customize their labels here
{ value: false, label: 'Hide' },
{ value: true, label: 'Show' },
],
});
```

#### Calling the flag

The function returned by `flag` can be called in two ways:

| Call signature | Description |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `flag()` | Evaluates the flag. The request is resolved automatically via TanStack Start's `getRequest()`. Use inside a loader, server function, or server route. |
| `flag(request, secret?)` | Evaluates the flag against an explicit `Request`. Use when evaluating outside of a request context. |
| `flag(code, flagsArray, secret?)` | Reads a precomputed value out of `code` instead of running `decide`. See [`precompute`](#precompute). |

`decide` and `identify` are deduplicated per request, so calling the same flag multiple times within one request only evaluates it once.

### `getProviderData`

**Description**: Turns flags declared using `flag` into Vercel Toolbar compatible definitions.

| Parameter | Type | Description |
| --------- | ---------------------- | ------------------------------------------------------------------- |
| `flags` | `Record<string, Flag>` | A record where the values are feature flags. The keys are not used. |

### `createFlagsDiscoveryEndpoint`

**Description**: Creates a handler for the `/.well-known/vercel/flags` discovery endpoint. It verifies the Vercel Toolbar's authenticated request and responds with the provided API data. Wire it up as a TanStack Start server route.

| Parameter | Type | Description |
| ------------------- | ---------------------------------------------------------- | -------------------------------------------------------- |
| `getApiData` | `(context) => ApiData \| Promise<ApiData>` | Returns the API data, typically via `getProviderData`. |
| `options` (Optional)| `{ secret?: string }` | The `FLAGS_SECRET` used to verify access. Defaults to the `FLAGS_SECRET` environment variable. |

```ts title="src/routes/[.]well-known/vercel/flags.ts"
import { createFileRoute } from '@tanstack/react-router';
import {
createFlagsDiscoveryEndpoint,
getProviderData,
} from 'flags/tanstack-start';
import * as flags from '../../../flags';

const handler = createFlagsDiscoveryEndpoint(() => getProviderData(flags));

export const Route = createFileRoute('/.well-known/vercel/flags')({
server: { handlers: { GET: handler } },
});
```

## Precomputation

These APIs are relevant for [precomputing feature flags](/principles/precompute). See the [marketing pages guide](/frameworks/tanstack-start/guides/marketing-pages) to learn how to use them in TanStack Start.

### `precompute`

**Description**: Evaluates multiple feature flags. Returns their values encoded into a single signed string.

| Parameter | Type | Description |
| ------------------- | ------------ | ---------------------------------------------------------------------------------------- |
| `flags` | `function[]` | The flags to precompute. |
| `request` | `Request` | The incoming request used to evaluate the flags. |
| `secret` (Optional) | `string` | The secret used to sign the generated code. Defaults to the `FLAGS_SECRET` env variable. |

Read the encoded values back by calling a flag with the generated code and the same flags array: `await myFlag(code, flags)`.

### `generatePermutations`

**Description**: Calculates all precomputations of the options of the provided flags. Useful when you want to prerender or statically cache every variant of a page.

| Parameter | Type | Description |
| ------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------- |
| `flags` | `function[]` | The flags. |
| `filter` (Optional) | `function` | Called with every possible permutation of the flags' options. Return `true` to keep the permutation. |
| `secret` (Optional) | `string` | The secret used to sign the generated codes. Defaults to the `FLAGS_SECRET` env variable. |

## Encryption

These functions keep flag data confidential (used by the Flags Explorer). All of them default to the `FLAGS_SECRET` environment variable when no secret is passed.

| Function | Description |
| ------------------------- | ------------------------------------ |
| `encryptFlagValues` | Encrypt resolved flag values. |
| `decryptFlagValues` | Decrypt flag values. |
| `encryptFlagDefinitions` | Encrypt flag definitions/metadata. |
| `decryptFlagDefinitions` | Decrypt flag definitions. |
| `encryptOverrides` | Encrypt toolbar overrides. |
| `decryptOverrides` | Decrypt toolbar overrides. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
title: Evaluation Context
description: Segment by any criteria, using an evaluation context
---

It is common for features to be on for some users, but off for others.
For example team members working on a new setting might need to see and
use the setting, while the rest of the team need the setting to be
hidden.

The `flag` declaration accepts an `identify`
function. The entities returned from the `identify` function
are passed as an argument to the `decide` function.

## Example

A trivial case to illustrate the concept:

```ts title="src/flags.ts"
import { flag } from 'flags/tanstack-start';

export const exampleFlag = flag<boolean>({
key: 'identify-example-flag',
identify() {
return { user: { id: 'user1' } };
},
decide({ entities }) {
return entities?.user?.id === 'user1';
},
});
```

Having first-class support for an evaluation context allows decoupling
the identifying step from the decision making step.

## Type safety

The entities can be typed using the `flag` function.

```ts title="src/flags.ts"
import { flag } from 'flags/tanstack-start';

interface Entities {
user?: { id: string };
}

export const exampleFlag = flag<boolean, Entities>({
key: 'identify-example-flag',
identify() {
return { user: { id: 'user1' } };
},
decide({ entities }) {
return entities?.user?.id === 'user1';
},
});
```

## Headers and Cookies

The `identify` function is called with `headers`{" "}
and `cookies` arguments, which is useful when dealing with
anonymous or authenticated users.

The arguments are normalized to a common format, so the same flag can be
used in Routing Middleware and within TanStack Start server contexts (route
loaders, server functions, server routes) without having to worry about the
differences in how `headers` and `cookies` are represented there.

```ts title="src/flags.ts"
import { flag } from 'flags/tanstack-start';

export const exampleFlag = flag<boolean, Entities>({
// ...
identify({ headers, cookies }) {
// access to normalized headers and cookies here
headers.get('auth');
cookies.get('auth')?.value;
// ...
},
// ...
});
```

## Deduplication

Calls to `identify` are deduped based on the object identity of the passed function. That means, in order to ensure that an `identify` function is only called once per request,
make sure to extract it to a named function and reuse it across your flags.

```ts title="src/flags.ts"
import type { ReadonlyHeaders, ReadonlyRequestCookies } from 'flags';
import { flag } from 'flags/tanstack-start';

interface Entities {
visitorId?: string;
}

function identify({
cookies,
headers,
}: {
cookies: ReadonlyRequestCookies;
headers: ReadonlyHeaders;
}): Entities {
const visitorId =
cookies.get('visitorId')?.value ?? headers.get('x-visitor-id');

return { visitorId };
}

export const exampleFlag1 = flag<boolean, Entities>({
key: 'exampleFlag1',
identify,
decide({ entities }) {
// ...
},
});

export const exampleFlag2 = flag<boolean, Entities>({
key: 'exampleFlag2',
identify,
decide({ entities }) {
// ...
},
});
```

## Precomputing and targeting

The [Marketing Pages](/frameworks/tanstack-start/guides/marketing-pages) example shows how to identify and target users using cookies when
precomputing pages.

### Full example

<LearnMore href="/frameworks/tanstack-start/guides/marketing-pages" icon="arrow">
See the Marketing Pages example
</LearnMore>
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
title: Dashboard Pages
description: Use feature flags on dynamic pages.
---

Dashboard pages are rendered at request time, and may require authenticated
users.

The example below shows how to use feature flags to show a feature to
specific users on a dashboard page. They are flagged in based on a specific cookie.

## Definition

The example works by first defining a feature flag that reads from a cookie.

```ts title="src/flags.ts"
import { flag } from 'flags/tanstack-start';

export const showNewDashboard = flag<boolean>({
key: 'showNewDashboard',
decide({ cookies }) {
return cookies.get('showNewDashboard')?.value === 'true';
},
});
```

The example reads the value directly from the cookie. In a real
dashboard you would likely read a signed JWT instead.

## Usage

Flags are evaluated on the server. Wrap the evaluation in a `createServerFn()`
server function and call it from the route loader — this keeps the flag working
during client-side navigation as well as on the initial server render.

```tsx title="src/routes/dashboard.tsx"
import { createFileRoute } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
import { showNewDashboard } from '../flags';

const getDashboardFlags = createServerFn().handler(async () => {
return { showNewDashboard: await showNewDashboard() };
});

export const Route = createFileRoute('/dashboard')({
loader: () => getDashboardFlags(),
component: Dashboard,
});

function Dashboard() {
const { showNewDashboard } = Route.useLoaderData();

return (
<main>
<h1>Dashboard</h1>
{showNewDashboard ? (
<p>You are seeing the new dashboard.</p>
) : (
<p>You are seeing the old dashboard.</p>
)}
</main>
);
}
```

Inside the server function the flag is called with no arguments — the request is
resolved automatically through TanStack Start's `getRequest()`. Since dashboard
pages are typically dynamic anyhow, the async call to evaluate the feature flag
fits right in.

## Evaluation Context

Feature flags used on dashboards will usually run in the Serverless
Function Region, close to the database. This means it is acceptable for
a feature flag's `decide` function to read the database
when establishing the evaluation context. However, ideally, it would
only read from the JWT as this leads to lower overall latency.
Loading