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.
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:
// 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>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:
Use cases:
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
User Context Options
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:
User Targeting
Target specific users or groups from your dashboard:
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:
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 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:
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:
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:
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
Ready to get started? Create your first feature flag in the dashboard →
How is this guide?