# Flags SDK

> Declare flags with the Vercel Flags SDK and evaluate them from the flags block, per user, per tenant and per custom context such as a brand, with precompute and Flags Explorer.

Source: https://bettersupabase.com/docs/flags-sdk

`better-supabase/flags-sdk` connects the [Flags SDK](https://flags-sdk.dev)
to the [flags block](/docs/blocks/flags). You declare flags with `flag()`
from `flags/next`, and the adapter answers them from the flags table with
the same rules, overrides and rollout buckets as `flag_enabled()` in your
policies.

```bash
pnpm add flags
pnpm better-supabase sql add flags
```

`flags` is an optional peer (`>=4.2 <5`). The entry types the SDK
structurally and never imports it, so it adds nothing to a bundle that
doesn't use it, and a type test fails when a new `flags` release changes the
`Adapter` shape. The entry imports no Node built-ins, so it runs in
`proxy.ts`.

| Export                  | Does                                                                                     |
| ----------------------- | ---------------------------------------------------------------------------------------- |
| `betterSupabaseAdapter` | an adapter factory over the flags block, with one definitions cache for every flag       |
| `createIdentify`        | an `identify` that returns the user, the tenant and custom contexts from verified claims |
| `getProviderData`       | Flags Explorer data for every flag that isn't archived                                   |
| `flagOptions`           | a flag's variants as `options`, which a precomputed flag needs                           |
| `flagContextOf`         | the evaluation context the adapter builds from the entities                              |

The [Next.js example](/docs/examples) wires all of them.

## Declaring flags [#declaring-flags]

`betterSupabaseAdapter` takes the options of `createFlagsProvider`: a
`transport` (with `schema` for the Data API) or fixed `definitions`, and a
`ttl` (30 seconds by default). It loads every flag with
`flag_definitions()`, which only the service role may call, so give it a
transport on the secret key. Never prefix that key with `NEXT_PUBLIC_`.

```ts title="src/flags.ts"
import "server-only";

import { createClient } from "@supabase/supabase-js";
import { rpcTransport } from "better-supabase/blocks";
import {
  betterSupabaseAdapter,
  createIdentify,
  type FlagsSdkEntities,
} from "better-supabase/flags-sdk";
import { dedupe, flag } from "flags/next";

const service = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SECRET_KEY!,
  { auth: { persistSession: false, autoRefreshToken: false } },
);

export const bsFlags = betterSupabaseAdapter({
  transport: rpcTransport(service),
  schema: "api",
  origin: "https://admin.acme.test/flags/{key}",
});

const identify = dedupe(
  createIdentify({
    claims: async ({ cookies }) => readVerifiedClaims(cookies),
    contexts: ({ headers }) => ({ brand: brandOf(headers.get("host")) }),
  }),
);

export const newEditor = flag<boolean, FlagsSdkEntities>({
  key: "new_editor",
  adapter: bsFlags(),
  defaultValue: false,
  identify,
});
```

Every adapter the factory makes shares one cache and one `adapterId`, so
`evaluate([...])` decides a batch from one load. A flag that is missing,
has another type or fails to load answers the declaration's
`defaultValue`, so a database outage turns features off instead of
breaking the page. Without a `defaultValue`, `decide` throws.

`origin` tells Flags Explorer where each flag is managed: a URL in which
`{key}` is replaced by the flag key, or a function of the key.

## Identifying the caller [#identifying-the-caller]

`createIdentify` builds on [`flagContext`](/docs/blocks/flags#with-openfeature).
It returns these entities:

| Entity         | Holds                                                                         |
| -------------- | ----------------------------------------------------------------------------- |
| `user`         | `{ id }`, the user id from the claims                                         |
| `organization` | `{ id, plans, roles }`, the active tenant with its features and roles         |
| `brand`, ...   | `{ id }` for each custom context, named as in `blocks.flags.options.contexts` |

`claims` must return claims you verified locally, such as from
`getClaims()` or `bs.context()`; `identify` runs for every flag and never
calls the Auth server. `tenant` picks the active tenant when the claims
don't carry it, and `contexts` adds custom context values from a header,
the host or a cookie. `contextClaims` from `flagContext` also works, such as
`{ brand: "brand_id" }`. Wrap the result in `dedupe` so it runs once per
request.

The entity names are the ones Vercel Flags targets (`user.id`,
`organization.id`, `brand.id`), so one `identify` serves both adapters. Move
a flag to [Vercel Flags](https://vercel.com/docs/flags) by changing its
adapter:

```ts
import { vercelAdapter } from "@flags-sdk/vercel";

export const checkoutTheme = flag<string, FlagsSdkEntities>({
  key: "checkout_theme",
  adapter: vercelAdapter(),
  defaultValue: "classic",
  identify,
});
```

The flags block is still what policies check, so keep a flag that guards
data in the flags block.

## Precompute [#precompute]

A [precomputed](https://flags-sdk.dev/docs/frameworks/next/precompute) flag
is evaluated once in `proxy.ts`, and the request is rewritten to a static
page for that combination of values. List the flag's `options` so
`generatePermutations` knows every combination; `flagOptions(definition)`
builds them from a flag's variants.

```ts title="src/proxy.ts"
import { precompute } from "flags/next";

import { pricingFlags } from "./flags";

if (path === "/pricing") {
  const code = await precompute(pricingFlags);
  return NextResponse.rewrite(new URL(`/pricing/${code}`, request.url));
}
```

```tsx title="app/pricing/[code]/page.tsx"
import { generatePermutations } from "flags/next";

import { pricingBanner, pricingFlags } from "@/flags";

export async function generateStaticParams() {
  const codes = await generatePermutations(pricingFlags);
  return codes.map((code) => ({ code }));
}

export default async function Pricing({
  params,
}: PageProps<"/pricing/[code]">) {
  const { code } = await params;
  const banner = await pricingBanner(code, pricingFlags);
  // ...
}
```

`precompute` signs the code with `FLAGS_SECRET`, a random 32-byte base64url
string. Set it in every environment (`openssl rand -base64url 32`).

Precompute only flags with a few values per visitor: a brand, a plan, a
rollout bucket. A per-user flag multiplies the permutations, and a static
page built for one user's variant can reach another user. Read those flags
dynamically.

## Dashboard pages [#dashboard-pages]

On a signed-in page, read the flag at request time, as in the
[dashboard pages guide](https://flags-sdk.dev/docs/frameworks/next/guides/dashboard-pages):

```tsx
const showEditor = await newEditor();
```

The flag only decides what the page shows. Keep the data behind it in a
policy with `flag_enabled()` or `tenant_ids_with_flag()`, so a request that
skips the page still meets the same flag:

```sql
create policy "drafts_insert" on public.drafts for insert to authenticated
  with check (better_supabase.flag_enabled('new_editor', organization_id));
```

The adapter and `flag_evaluation()` agree, with one difference: the adapter
never reads a custom context from the tenant table, so pass it through
`identify` when a rule targets it.

## Flags Explorer [#flags-explorer]

[Flags Explorer](https://vercel.com/docs/feature-flags/flags-explorer) in
the Vercel Toolbar reads the flags from a discovery route. `getProviderData`
lists every flag in the flags block that isn't archived, with its
description, options, origin and last change; merge it with the declared
flags:

```ts title="app/.well-known/vercel/flags/route.ts"
import { getProviderData } from "better-supabase/flags-sdk";
import { mergeProviderData } from "flags";
import {
  createFlagsDiscoveryEndpoint,
  getProviderData as getDeclared,
} from "flags/next";

import { bsFlags, newEditor, pricingBanner } from "@/flags";

export const GET = createFlagsDiscoveryEndpoint(async () =>
  mergeProviderData([
    getDeclared({ newEditor, pricingBanner }),
    await getProviderData(bsFlags),
  ]),
);
```

The route answers only requests signed with `FLAGS_SECRET`. A failed load
shows as a hint in Explorer instead of an error. Exclude
`/.well-known/vercel/` from the proxy matcher. An override set in the
toolbar applies before the adapter's `decide`.

## Reporting exposures [#reporting-exposures]

`flag()` reports each value to Vercel Analytics. To send evaluations
elsewhere, wrap the adapter. This one sends PostHog's
`$feature_flag_called` event:

```ts
import type { FlagsSdkAdapter } from "better-supabase/flags-sdk";

function reported<V>(adapter: FlagsSdkAdapter<V>): FlagsSdkAdapter<V> {
  return {
    ...adapter,
    async decide(params) {
      const value = await adapter.decide(params);
      posthog.capture({
        distinctId: params.entities?.user?.id ?? "anonymous",
        event: "$feature_flag_called",
        properties: {
          $feature_flag: params.key,
          $feature_flag_response: value,
        },
      });
      return value;
    },
  };
}
```

For OpenTelemetry, record a `feature_flag.evaluation` event with
`feature_flag.key` and `feature_flag.result.value` on the active span in the
same place.

## Not built in [#not-built-in]

These are left to the app or to other tools for now:

- Reusable segments, such as a list of beta customers that many rules share.
  A rule lists its tenants, users, plans, roles and contexts itself.
- Self-serve previews that a tenant admin turns on. Build them as a flag
  with tenant overrides that your settings page writes.
- Scheduled changes. Run `flags.save` from a [job](/docs/blocks/jobs).
- Approval before a change. A `save` hook can require one (see
  [extending blocks](/docs/extending/blocks)).
- Experiments and metrics. Use PostHog or Statsig through their own Flags
  SDK adapters.
- Brands as a table of their own. A brand is a custom context read from your
  tenant table (see [custom contexts](/docs/blocks/flags#custom-contexts)).