# Feature Flags (Client)

> Control feature rollouts and variant splits with React and Vue hooks


import { Tab, Tabs } from "@/components/docs";
import { Callout } from "@/components/docs";
import { CodeBlock } from "@/components/docs";
import { Card, Cards } from "@/components/docs";

Feature flags allow you to control feature rollouts and split people across variants without deploying new code. Enable or disable features instantly from your dashboard.

<Callout type="info">
  **Dashboard-controlled Rollouts** | **Client-side Caching** | **Stale-While-Revalidate**
</Callout>

## Quick Start

<Tabs items={['React with User Context', 'Vue', 'Setup']}>

<Tab>
<CodeBlock language="tsx" filename="app.tsx">
  {`'use client';

  import { FlagsProvider, useFlag } from '@databuddy/sdk/react';
  // Replace with your app's auth/session hook.
  import { useCurrentUser } from '@/lib/auth-client';

  function App() {
    const { user, isLoading } = useCurrentUser();

    return (
      <FlagsProvider
        clientId={process.env.NEXT_PUBLIC_DATABUDDY_CLIENT_ID!}
        apiUrl="https://api.databuddy.cc"
        isPending={isLoading}
        user={user ? {
          userId: user.id,
          organizationId: user.organizationId,
          teamId: user.teamId,
          properties: {
            role: user.role,
            workspace_type: user.workspaceType,
            region: user.region,
          }
        } : undefined}
      >
        <MyComponent />
      </FlagsProvider>
    );
  }

  function MyComponent() {
    const { on: isDarkMode, loading } = useFlag('dark-mode');
    const { on: showNewDashboard } = useFlag('new-dashboard');

    if (loading) return <Skeleton />;

    return (
      <div className={isDarkMode ? 'dark' : ''}>
        {showNewDashboard ? <NewDashboard /> : <OldDashboard />}
      </div>
    );
  }`}
</CodeBlock>
</Tab>

<Tab>
<CodeBlock language="ts" filename="main.ts">
  {`import { createApp } from 'vue';
  import { createFlagsPlugin } from '@databuddy/sdk/vue';
  import App from './App.vue';

  // currentUser should come from your app's auth/session store.
  createApp(App)
    .use(createFlagsPlugin({
      clientId: import.meta.env.VITE_DATABUDDY_CLIENT_ID,
      apiUrl: 'https://api.databuddy.cc',
      user: {
        userId: currentUser.id,
        organizationId: currentUser.organizationId,
        teamId: currentUser.teamId,
        properties: {
          role: currentUser.role,
          workspace_type: currentUser.workspaceType,
        },
      },
    }))
    .mount('#app');`}
</CodeBlock>

<CodeBlock language="vue" filename="FeatureGate.vue">
  {`<script setup lang="ts">
  import { useFlag } from '@databuddy/sdk/vue';

  const newNav = useFlag('new-nav');
  </script>

  <template>
    <Skeleton v-if="newNav.loading.value" />
    <NewNav v-else-if="newNav.on.value" />
    <OldNav v-else />
  </template>`}
</CodeBlock>
</Tab>

<Tab>
<CodeBlock language="bash" filename="terminal">
  {`# Install the SDK
  bun add @databuddy/sdk`}
</CodeBlock>
</Tab>

</Tabs>

## Hooks API

<Callout type="info">
  **React vs Vue API differences**: In Vue, `useFlag` returns `{ on, loading, state }` as refs; there are no top-level `status`, `value`, or `variant` fields, so read them from `state` (e.g. `flag.state.value.variant`). Vue's `useFlags` returns `{ getFlag, fetchAllFlags, updateUser, refresh, updateConfig, memoryFlags, lastError }`; it has no `isOn`, `getValue`, `fetchFlag`, or `isReady`. The `getValue`, `isOn`, and `variant` examples below are React-only.
</Callout>

### `useFlag(key)` - Flag State

The primary hook for checking feature flags. Returns `{ on, loading, status, value, variant }`.

<CodeBlock language="tsx">
  {`import { useFlag } from '@databuddy/sdk/react';

  function MyComponent() {
    const { on, loading } = useFlag('new-dashboard');

    if (loading) return <Skeleton />;
    return on ? <NewDashboard /> : <OldDashboard />;
  }`}
</CodeBlock>

You can also use the `status` field for more granular control:

<CodeBlock language="tsx">
  {`import { useFlag } from '@databuddy/sdk/react';

  function MyComponent() {
    const flag = useFlag('experiment');

    switch (flag.status) {
      case 'loading':
        return <Skeleton />;
      case 'error':
        return <ErrorFallback />;
      case 'ready':
        return flag.on ? <NewFeature /> : <OldFeature />;
      case 'pending':
        return <LoadingState />;
    }
  }`}
</CodeBlock>

### `useFlags()` - Context API

Access the full flags context for advanced use cases like checking multiple flags, fetching typed values, or updating user context.

<CodeBlock language="tsx">
  {`import { useFlags } from '@databuddy/sdk/react';

  function MyComponent() {
    const {
      isOn,           // Simple boolean check
      getFlag,        // Get full flag state
      getValue,       // Get typed value
      fetchFlag,      // Async fetch single flag
      fetchAllFlags,  // Async fetch all flags
      updateUser,     // Update user context
      refresh,        // Refresh all flags
      isReady         // SDK ready state
    } = useFlags();

    // Simple boolean check
    if (isOn('advanced-feature')) {
      // Show the advanced experience
    }

    // Typed values (string, number, boolean)
    const maxItems = getValue<number>('max-items', 10);
    const theme = getValue<'light' | 'dark'>('theme', 'light');

    // Get full state
    const flag = getFlag('experiment');
    console.log(flag.on, flag.loading, flag.status);

    // A/B test variants
    const variant = getFlag('export-flow').variant;
  }`}
</CodeBlock>

## Flag Types

### Boolean Flags
Simple on/off switches for features.

<CodeBlock language="tsx">
  {`const { on } = useFlag('my-feature');`}
</CodeBlock>

### String/Number Flags
For configuration values and multivariate testing. Use `getValue` from the context:

<CodeBlock language="tsx">
  {`const { getValue } = useFlags();
  const maxUsers = getValue<number>('max-users', 100);
  const theme = getValue<'light' | 'dark'>('theme', 'light');`}
</CodeBlock>

### Rollout Flags
Gradually roll out features to a percentage of users.

<CodeBlock language="tsx">
  {`// Set rollout percentage in dashboard (e.g., 25%)
  const { on } = useFlag('new-ui-rollout');`}
</CodeBlock>

#### Rollout Units

Control how users are grouped for rollouts. In the dashboard, choose the **Rollout Unit**:

| Unit | Description | Use Case |
|------|-------------|----------|
| **User** | Each user individually | Standard per-user rollouts |
| **Organization** | All org members together | Organization-wide launches |
| **Team** | All team members together | Team-specific features |

<CodeBlock language="tsx" filename="org-rollout.tsx">
  {`// Pass organizationId for org-level rollouts
  <FlagsProvider
    clientId="your-client-id"
    user={{
      userId: "user_123",
      organizationId: "org_abc",  // All org members get same result
      teamId: "team_456",         // All team members get same result
    }}
  >
    <App />
  </FlagsProvider>`}
</CodeBlock>

<Callout type="info">
  **Consistent Group Rollouts**: When using organization or team rollouts, all members with the same ID get the same flag result. If `org_abc` falls within the 25% rollout, all its members see the feature.
</Callout>

### Multivariant Flags (A/B/n Testing)

Split people across multiple variants. Each user is consistently assigned to a variant based on their identifier. There's no built-in results view yet, so compare variants with your own events. Use the `variant` field from `useFlag`:

<CodeBlock language="tsx" filename="export-flow.tsx">
  {`import { useFlag } from '@databuddy/sdk/react';

  function ExportPanel() {
    const { variant, loading } = useFlag('export-flow');

    if (loading) return <Skeleton />;

    switch (variant) {
      case 'control':
        return <DefaultExportPanel />;
      case 'compact':
        return <CompactExportPanel />;
      case 'guided':
        return <GuidedExportPanel />;
      default:
        return <DefaultExportPanel />;
    }
  }`}
</CodeBlock>

**Creating multivariant flags in the dashboard:**

1. Select "multivariant" as the flag type
2. Add your variants with keys and values (string, number, or JSON)
3. Add traffic weights when you need a weighted split, or omit weights for even assignment
4. Avoid using force targeting rules to assign variants; rules are gates or overrides, not variant selectors

<Callout type="info">
  **Consistent Assignment**: Users are deterministically assigned to variants based on their userId or email, ensuring they always see the same variant across sessions.
</Callout>

<Callout type="info">
  **Variant weights**: If no variant weights are configured, users are split evenly. If you add weights, they must total exactly 100; the dashboard rejects flags whose weights do not sum to 100.
</Callout>

**Use cases:**
- **A/B Testing**: Compare two versions of a feature
- **UI variations**: Test layouts, colors, copy
- **Onboarding flows**: Compare guided vs. compact setup paths
- **Feature configuration**: Ship string, number, or JSON values without a deploy
- **A/B/n experiments**: Run experiments with 3+ variants

## Configuration

<CodeBlock language="tsx" filename="provider.tsx">
  {`<FlagsProvider
    clientId="your-client-id"
    apiUrl="https://api.databuddy.cc"
    user={{
      userId: currentUser.id,
      organizationId: currentUser.organizationId,  // For org-level rollouts
      teamId: currentUser.teamId,                  // For team-level rollouts
      properties: {
        role: currentUser.role || 'user',
        workspace_type: currentUser.workspaceType || 'personal',
        region: currentUser.region || 'us-east',
        created_at: currentUser.createdAt,
      }
    }}
    isPending={isLoadingSession}
    debug={process.env.NODE_ENV === 'development'}
    autoFetch={true}
    cacheTtl={60000}        // Cache for 1 minute
    staleTime={30000}       // Revalidate after 30s
  >
    <App />
  </FlagsProvider>`}
</CodeBlock>

### Configuration Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `clientId` | string | Required | Your Databuddy Client ID from the dashboard |
| `apiUrl` | string | `https://api.databuddy.cc` | API endpoint |
| `user` | object | `undefined` | User context for targeting |
| `isPending` | boolean | `false` | Defer evaluation while session loads |
| `disabled` | boolean | `false` | Disable all flag evaluation |
| `debug` | boolean | `false` | Enable debug logging |
| `environment` | string | `undefined` | Environment name |
| `cacheTtl` | number | `60000` | Cache TTL in ms (1 minute) |
| `staleTime` | number | `30000` | Revalidate after (30 seconds) |
| `autoFetch` | boolean | `true` | Auto-fetch flags on mount |
| `skipStorage` | boolean | `false` | Skip localStorage caching |

### User Context Options

| Option | Type | Description |
|--------|------|-------------|
| `userId` | string | Unique user identifier for per-user rollouts |
| `email` | string | User email (alternative identifier) |
| `organizationId` | string | Organization ID for org-level rollouts |
| `teamId` | string | Team ID for team-level rollouts |
| `properties` | object | Custom properties for targeting rules |

### Environments

Pass `environment` when you maintain separate flag definitions for production, staging, or previews. If `environment` is omitted, the SDK fetches definitions that do not have an environment assigned.

<CodeBlock language="tsx">
  {`<FlagsProvider
    clientId={process.env.NEXT_PUBLIC_DATABUDDY_CLIENT_ID!}
    environment={process.env.NEXT_PUBLIC_VERCEL_ENV ?? "production"}
  >
    <App />
  </FlagsProvider>`}
</CodeBlock>

## Flag States

The `FlagState` object returned by `useFlag`:

<CodeBlock language="tsx">
  {`interface FlagState {
    on: boolean;              // Whether the flag is enabled
    loading: boolean;         // Whether the flag is loading
    status: FlagStatus;       // 'loading' | 'ready' | 'error' | 'pending'
    value?: boolean | string | number;  // The flag's value
    variant?: string;         // Variant for A/B tests
  }`}
</CodeBlock>

## Performance Features

### Stale-While-Revalidate
Flags return cached values immediately while revalidating in the background.

<CodeBlock language="tsx">
  {`// Returns cached value instantly, revalidates if stale
  const { on } = useFlag('my-feature');  // Instant!`}
</CodeBlock>

### Request Batching
Multiple flag requests within 10ms are batched into a single API call.

<CodeBlock language="tsx">
  {`// These 3 calls become 1 API request
  const feature1 = useFlag('feature-1');
  const feature2 = useFlag('feature-2');
  const feature3 = useFlag('feature-3');`}
</CodeBlock>

### Visibility API
Pauses background revalidation when the tab is hidden to save bandwidth and battery. Initial fetches still run.

### Request Deduplication
Identical requests are deduplicated automatically.

## Why isPending Matters

The `isPending` prop prevents race conditions during authentication:

<CodeBlock language="tsx">
  {`// Bad: Flags evaluate with wrong user context
  <FlagsProvider user={undefined}>
    <App /> // Shows anonymous features, then switches
  </FlagsProvider>

  // Good: Waits for session before evaluating
  <FlagsProvider
    isPending={isPending}
    user={session?.user ? {...} : undefined}
  >
    <App /> // Shows correct features immediately
  </FlagsProvider>`}
</CodeBlock>

**Benefits:**
- Prevents flash of incorrect content
- Avoids unnecessary API calls
- Consistent user experience
- Better performance

## User Targeting

Target specific users or groups from your dashboard:

- **User ID**: Target specific users by their unique identifier
- **Email**: Target by email address or domain (e.g., `@company.com`)
- **Organization**: Roll out to entire organizations at once
- **Team**: Roll out to specific teams
- **Custom Properties**: Target by user attributes

### Organization & Team Rollouts

For launches that should apply to all members of an organization:

<CodeBlock language="tsx">
  {`<FlagsProvider
    user={{
      userId: "user-123",
      organizationId: "org_abc",  // All org members get same result
      teamId: "team_456",         // All team members get same result
    }}
  >`}
</CodeBlock>

Then in the dashboard, set the **Rollout Unit** to "Organization" or "Team" when configuring your rollout flag.

### Target Groups

Target groups are reusable audiences you can attach to flags. They can match:

- `userId`
- `email`
- custom `properties` fields

Supported rule operators include `equals`, `contains`, `starts_with`, `ends_with`, `in`, and `not_in`. Property rules also support `exists` and `not_exists`.

Evaluation order matters:

1. Direct user targeting rules
2. Attached target group rules
3. Multivariant assignment
4. Rollout percentage
5. Boolean default value

Direct rules and target group matches short-circuit evaluation as force on/off rules. For A/B/n experiments, use them to control eligibility carefully, but do not rely on them to choose a specific variant. If you need both strict eligibility and variant measurement, use a boolean eligibility flag plus a separate multivariant assignment flag.

### Advanced Targeting with Custom Properties

<CodeBlock language="tsx">
  {`<FlagsProvider
    user={{
      userId: "user-123",
      organizationId: "org_abc",
      teamId: "team_456",
      properties: {
        role: 'admin',             // Role-based rollouts
        workspace_type: 'team',    // Workspace targeting
        region: 'us-east',         // Geographic targeting
        created_at: '2024-01-01',  // Time-based targeting
        cohort: 'activation-a',    // Experiment or onboarding cohort
        featureUsage: {
          reportsViewed: 25        // Behavioral targeting
        }
      }
    }}
  >`}
</CodeBlock>

**Targeting Examples:**
- **Role-based**: `role: 'admin'` → Show admin workflows
- **Workspace-based**: `workspace_type: 'team'` → Group-specific experience
- **Geographic**: `region: 'us-east'` → Test in specific regions
- **Behavioral**: `reportsViewed > 10` → Target power users
- **Time-based**: `created_at > '2024-01-01'` → New vs. existing users

## Best Practices

### Measure Flag Outcomes

Treat flag metrics as two separate questions: who was evaluated, and what changed afterward. When the tracker is available, the browser SDK emits a deduplicated `$flag_evaluated` event when a flag is read through `useFlag`, `useFlags`, or the manager APIs. The Feature Flags dashboard uses those events to show observed visitors, identified visitors, evaluation count, and the last observed evaluation time.

These dashboard numbers are directional estimates, not guaranteed complete or exact counts of people who used a feature. They only include browser evaluations that Databuddy observed, so blockers, consent or opt-out settings, event sampling, missing tracker coverage, server-side evaluations, and incomplete identity linkage can make the numbers lower than reality. The visitor count should be read as “observed visitors evaluated,” not “people who used the feature.” Passing `userId` to the flags provider and calling Databuddy `identify()` on the tracker lets those evaluations carry your profile ID.

An evaluation is not proof that a feature was rendered or used, and server-side flag evaluations are not automatically sent as analytics events. For experiments where rendered exposure or product usage must be measured, track one explicit exposure event when the meaningful flagged view appears, then track the downstream outcome.

<CodeBlock language="tsx">
  {`import { track } from "@databuddy/sdk";
import { useEffect, useRef } from "react";
import { useFlag } from "@databuddy/sdk/react";

function ExportPanel() {
  const exportFlow = useFlag("export-flow");
  const trackedExposure = useRef(false);

  useEffect(() => {
    if (exportFlow.loading || trackedExposure.current) {
      return;
    }

    trackedExposure.current = true;
    track("feature_exposed", {
      flag_key: "export-flow",
      variant: exportFlow.variant ?? "control",
      feature: "report_export",
    });
  }, [exportFlow.loading, exportFlow.variant]);

  return exportFlow.variant === "compact" ? (
    <CompactExportPanel />
  ) : (
    <DefaultExportPanel />
  );
}`}
</CodeBlock>

Then track the downstream outcome and include the same flag key and variant.

<CodeBlock language="tsx">
  {`import { track } from "@databuddy/sdk";
import { useFlag } from "@databuddy/sdk/react";

function ExportButton() {
  const exportFlow = useFlag("export-flow");

  async function handleExport() {
    const result = await runExport();

    track("report_exported", {
      flag_key: "export-flow",
      variant: exportFlow.variant ?? "control",
      status: result.ok ? "success" : "failed",
      format: result.format,
    });
  }

  return exportFlow.variant === "compact" ? (
    <CompactExportButton onClick={handleExport} />
  ) : (
    <DefaultExportButton onClick={handleExport} />
  );
}`}
</CodeBlock>

Use one meaningful exposure event and one meaningful outcome event. Avoid tracking every render of a flagged component.

### Flag Planning Checklist

Before adding a flag, decide:

- **Owner**: who removes or expands the flag later
- **Default**: what users see if evaluation fails
- **Rollout unit**: user, organization, or team
- **Targeting inputs**: which IDs and low-cardinality properties are required
- **Success metric**: the one outcome event that proves the rollout helped
- **Cleanup date**: when the flag should be removed or made permanent

### Use the Right API

<CodeBlock language="tsx">
  {`// Simple boolean check — most common
  const { on, loading } = useFlag('dark-mode');

  // Full control with status
  const flag = useFlag('experiment');
  if (flag.status === 'ready') { ... }

  // A/B test variants
  const { variant } = useFlag('export-flow');

  // Typed values via context
  const { getValue } = useFlags();
  const maxItems = getValue<number>('max-items', 10);

  // Quick boolean via context (no loading state)
  const { isOn } = useFlags();
  if (isOn('advanced-search')) { ... }`}
</CodeBlock>

### Handle Loading States

<CodeBlock language="tsx">
  {`function FeatureComponent() {
    const { on, loading } = useFlag('new-feature');

    if (loading) return <Skeleton />;
    return on ? <NewFeature /> : <OldFeature />;
  }`}
</CodeBlock>

### Multiple Flags

<CodeBlock language="tsx">
  {`function Dashboard() {
    const { on: darkMode } = useFlag('dark-mode');
    const { on: newLayout } = useFlag('new-layout');
    const { getValue } = useFlags();
    const maxItems = getValue<number>('max-items', 10);

    return (
      <div className={darkMode ? 'dark' : ''}>
        {newLayout ? <NewLayout items={maxItems} /> : <OldLayout />}
      </div>
    );
  }`}
</CodeBlock>

### Update User Context

<CodeBlock language="tsx">
  {`function WorkspaceSwitcher() {
    const { updateUser, refresh } = useFlags();

    const handleWorkspaceChange = async (workspace: Workspace) => {
      // Update user context to refresh flags
      updateUser({
        userId: user.id,
        organizationId: workspace.organizationId,
        teamId: workspace.teamId,
        properties: {
          role: workspace.role,
          workspace_type: workspace.type
        }
      });

      // Clear stored flag values when switching between unrelated workspaces.
      await refresh(true);
    };

    return <WorkspaceMenu onChange={handleWorkspaceChange} />;
  }`}
</CodeBlock>

## Debug With DevTools

Install DevTools in local development to inspect the flag manager that `FlagsProvider` or Vue `createFlagsPlugin` mounted on the page.

<CodeBlock language="bash">
  {`bun add -d @databuddy/devtools`}
</CodeBlock>

<CodeBlock language="tsx">
  {`import { DatabuddyDevtools } from "@databuddy/devtools/react";

export function AppShell({ children }: { children: React.ReactNode }) {
  return (
    <>
      {children}
      <DatabuddyDevtools enabled={process.env.NODE_ENV !== "production"} />
    </>
  );
}`}
</CodeBlock>

Use the flag panel to verify:

- `clientId`, API host, environment, and readiness
- user context used for targeting
- evaluated values, variants, reasons, and sources
- whether a value came from API, cache, default, error, or local override
- local overrides before you ship a UI branch

When you paste an API key with `manage:flags` scope at runtime, DevTools can also create, edit, and delete flag definitions. Do not hard-code management keys in source, and clear local overrides before validating production-like behavior.

## Debug Mode

Enable debug mode to see flag evaluation in the console:

<CodeBlock language="tsx">
  {`<FlagsProvider
    debug={true}
    // ... other props
  >`}
</CodeBlock>

## Related

<Cards>
  <Card title="Server-Side Flags" href="/docs/sdk/server-flags">
    Feature flags for Node.js and API routes
  </Card>
  <Card title="React SDK" href="/docs/sdk/react">
    React component and integration
  </Card>
  <Card title="Vue SDK" href="/docs/sdk/vue">
    Vue component and composables
  </Card>
  <Card title="Configuration" href="/docs/sdk/configuration">
    All configuration options
  </Card>
  <Card title="DevTools" href="/docs/sdk/devtools">
    Inspect flag values, variants, and local overrides
  </Card>
</Cards>

---

Ready to get started? [Create your first feature flag in the dashboard →](https://app.databuddy.cc/register)
