Feature Flags (Client)
Feature flags allow you to control feature rollouts and conduct A/B testing without deploying new code. Enable or disable features instantly from your dashboard.
Dashboard-controlled Rollouts | Client-side Caching | Stale-While-Revalidate
Quick Start
'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>
);
}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');<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># Install the SDK
bun add @databuddy/sdkHooks API
useFlag(key) - Flag State
The primary hook for checking feature flags. Returns { on, loading, status, value, variant }.
import { useFlag } from '@databuddy/sdk/react';
function MyComponent() {
const { on, loading } = useFlag('new-dashboard');
if (loading) return <Skeleton />;
return on ? <NewDashboard /> : <OldDashboard />;
}You can also use the status field for more granular control:
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 />;
}
}useFlags() - Context API
Access the full flags context for advanced use cases like checking multiple flags, fetching typed values, or updating user context.
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;
}Flag Types
Boolean Flags
Simple on/off switches for features.
const { on } = useFlag('my-feature');String/Number Flags
For configuration values and multivariate testing. Use getValue from the context:
const { getValue } = useFlags();
const maxUsers = getValue<number>('max-users', 100);
const theme = getValue<'light' | 'dark'>('theme', 'light');Rollout Flags
Gradually roll out features to a percentage of users.
// Set rollout percentage in dashboard (e.g., 25%)
const { on } = useFlag('new-ui-rollout');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 |
// 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>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.
Multivariant Flags (A/B/n Testing)
Run experiments with multiple variants. Each user is consistently assigned to a variant based on their identifier. Use the variant field from useFlag:
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 />;
}
}Creating multivariant flags in the dashboard:
- Select "multivariant" as the flag type
- Add your variants with keys and values (string, number, or JSON)
- Add traffic weights when you need a weighted split, or omit weights for even assignment
- Avoid using force targeting rules to assign variants; rules are gates or overrides, not variant selectors
Consistent Assignment: Users are deterministically assigned to variants based on their userId or email, ensuring they always see the same variant across sessions.
Variant weights: If no variant weights are configured, users are split evenly. If you add weights, keep the totals easy to reason about, commonly 100.
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
<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>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.
<FlagsProvider
clientId={process.env.NEXT_PUBLIC_DATABUDDY_CLIENT_ID!}
environment={process.env.NEXT_PUBLIC_VERCEL_ENV ?? "production"}
>
<App />
</FlagsProvider>Flag States
The FlagState object returned by useFlag:
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
}Performance Features
Stale-While-Revalidate
Flags return cached values immediately while revalidating in the background.
// Returns cached value instantly, revalidates if stale
const { on } = useFlag('my-feature'); // Instant!Request Batching
Multiple flag requests within 10ms are batched into a single API call.
// These 3 calls become 1 API request
const feature1 = useFlag('feature-1');
const feature2 = useFlag('feature-2');
const feature3 = useFlag('feature-3');Visibility API
Pauses fetching when tab is hidden to save bandwidth and battery.
Request Deduplication
Identical requests are deduplicated automatically.
Why isPending Matters
The isPending prop prevents race conditions during authentication:
// 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>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:
<FlagsProvider
user={{
userId: "user-123",
organizationId: "org_abc", // All org members get same result
teamId: "team_456", // All team members get same result
}}
>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:
userIdemail- custom
propertiesfields
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:
- Direct user targeting rules
- Attached target group rules
- Multivariant assignment
- Rollout percentage
- 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
<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
}
}
}}
>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 exposed, and what changed afterward. The browser SDK emits $flag_evaluated for single-flag fetches when the tracker is available, but bulk-prefetched or cached reads may not emit a fresh exposure event. For experiments where exposure accounting must be complete, track one explicit exposure event when the meaningful flagged view appears.
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 />
);
}Then track the downstream outcome and include the same flag key and variant.
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} />
);
}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
// 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')) { ... }Handle Loading States
function FeatureComponent() {
const { on, loading } = useFlag('new-feature');
if (loading) return <Skeleton />;
return on ? <NewFeature /> : <OldFeature />;
}Multiple Flags
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>
);
}Update User Context
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} />;
}Debug With DevTools
Install DevTools in local development to inspect the flag manager that FlagsProvider or Vue createFlagsPlugin mounted on the page.
bun add -d @databuddy/devtoolsimport { DatabuddyDevtools } from "@databuddy/devtools/react";
export function AppShell({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<DatabuddyDevtools enabled={process.env.NODE_ENV !== "production"} />
</>
);
}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:
<FlagsProvider
debug={true}
// ... other props
>Related
Feature flags for Node.js and API routes
React component and integration
Vue component and composables
All configuration options
Inspect flag values, variants, and local overrides
Ready to get started? Create your first feature flag in the dashboard →
How is this guide?