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.
Real-time Updates | Client-side Caching | Stale-While-Revalidate
Quick Start
'use client';
import { FlagsProvider, useFeature } from '@databuddy/sdk/react';
import { useSession } from '@databuddy/auth/client';
function App() {
const { data: session, isPending } = useSession();
return (
<FlagsProvider
clientId="your-website-id"
apiUrl="https://api.databuddy.cc"
isPending={isPending}
user={session?.user ? {
userId: session.user.id,
email: session.user.email,
organizationId: session.user.organizationId,
teamId: session.user.teamId,
properties: {
plan: session.user.plan || 'free',
role: session.user.role || 'user',
region: 'us-east',
}
} : undefined}
>
<MyComponent />
</FlagsProvider>
);
}
function MyComponent() {
const { on: isDarkMode, loading } = useFeature('dark-mode');
const { on: showNewDashboard } = useFeature('new-dashboard');
if (loading) return <Skeleton />;
return (
<div className={isDarkMode ? 'dark' : ''}>
{showNewDashboard ? <NewDashboard /> : <OldDashboard />}
</div>
);
}Hooks API
useFeature(key) - Simple Feature Check
The simplest way to check if a feature is enabled. Returns an object with { on, loading, status, value, variant }.
import { useFeature } from '@databuddy/sdk/react';
function MyComponent() {
const { on, loading } = useFeature('new-dashboard');
if (loading) return <Skeleton />;
return on ? <NewDashboard /> : <OldDashboard />;
}useFeatureOn(key, defaultValue) - SSR-Safe Boolean
Returns a boolean directly with a default value. Perfect for SSR where you need an immediate value.
import { useFeatureOn } from '@databuddy/sdk/react';
function MyComponent() {
// Returns false while loading, then the actual value
const isDarkMode = useFeatureOn('dark-mode', false);
return (
<div className={isDarkMode ? 'dark' : 'light'}>
Content
</div>
);
}useFlag(key) - Full State Control
Get the complete flag state with all properties including status and error handling.
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 />;
}
}useFlagValue(key, defaultValue) - Typed Values
Get typed values for string or number flags.
import { useFlagValue } from '@databuddy/sdk/react';
function MyComponent() {
const maxItems = useFlagValue('max-items', 10);
const theme = useFlagValue<'light' | 'dark'>('theme', 'light');
return (
<div className={theme}>
Showing {maxItems} items
</div>
);
}useVariant(key) - A/B Testing
Get the variant name for multivariate testing.
import { useVariant } from '@databuddy/sdk/react';
function CheckoutPage() {
const variant = useVariant('checkout-experiment');
if (variant === 'control') return <OldCheckout />;
if (variant === 'variant-a') return <NewCheckoutA />;
if (variant === 'variant-b') return <NewCheckoutB />;
return <DefaultCheckout />;
}Flag Types
Boolean Flags
Simple on/off switches for features.
const { on } = useFeature('my-feature');String/Number Flags
For configuration values and multivariate testing.
const maxUsers = useFlagValue('max-users', 100);
const theme = useFlagValue<'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 } = useFeature('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 | Enterprise features, billing changes |
| Team | All team members together | Team-specific features |
// Pass organizationId for org-level rollouts
<FlagsProvider
clientId="your-website-id"
user={{
userId: "user_123",
email: "user@company.com",
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.
import { useVariant, useFeature } from '@databuddy/sdk/react';
function CheckoutPage() {
const variant = useVariant('checkout-experiment');
const { value, loading } = useFeature('checkout-experiment');
if (loading) return <Skeleton />;
// Variant-based rendering
switch (variant) {
case 'control':
return <CurrentCheckout />;
case 'simplified':
return <SimplifiedCheckout />;
case 'one-click':
return <OneClickCheckout />;
default:
return <CurrentCheckout />;
}
}Creating multivariant flags in the dashboard:
- Select "multivariant" as the flag type
- Add your variants with keys and values (string, number, or JSON)
- Set traffic weights for each variant (must sum to 100%)
- Optionally add targeting rules to show specific variants to certain users
Consistent Assignment: Users are deterministically assigned to variants based on their userId or email, ensuring they always see the same variant across sessions.
Use cases:
- A/B Testing: Compare two versions of a feature
- Pricing experiments: Test different price points
- UI variations: Test layouts, colors, copy
- Multi-armed bandits: Run experiments with 3+ variants
Configuration
<FlagsProvider
clientId="your-website-id"
apiUrl="https://api.databuddy.cc"
user={{
userId: currentUser.id,
email: currentUser.email,
organizationId: currentUser.organizationId, // For org-level rollouts
teamId: currentUser.teamId, // For team-level rollouts
properties: {
plan: currentUser.plan || 'free',
role: currentUser.role || 'user',
region: currentUser.region || 'us-east',
signupDate: 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 website 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 |
Advanced Context API
Access the flags context directly for more control:
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 check
if (isOn('premium-feature')) {
// Show premium content
}
// Get full state
const flag = getFlag('experiment');
console.log(flag.on, flag.loading, flag.status);
// Refresh flags
const handleRefresh = async () => {
await refresh();
};
// Update user context
const handleUpgrade = async () => {
updateUser({
userId: user.id,
email: user.email,
properties: { plan: 'premium' }
});
};
}Flag States
The FlagState object has the following properties:
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
// Deprecated (use above instead):
enabled: boolean; // Use 'on' instead
isLoading: boolean; // Use 'loading' instead
isReady: boolean; // Use 'status === "ready"' instead
}Performance Features
Stale-While-Revalidate
Flags return cached values immediately while revalidating in the background.
// Returns cached value instantly, revalidates if stale
const { on } = useFeature('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 = useFeature('feature-1');
const feature2 = useFeature('feature-2');
const feature3 = useFeature('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 enterprise features or billing changes that should apply to all members of an organization:
<FlagsProvider
user={{
userId: "user-123",
email: "user@company.com",
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.
Advanced Targeting with Custom Properties
<FlagsProvider
user={{
userId: "user-123",
email: "user@example.com",
organizationId: "org_abc",
teamId: "team_456",
properties: {
plan: 'premium', // Plan-based rollouts
region: 'us-east', // Geographic targeting
signupDate: '2024-01-01', // Time-based targeting
experimentGroup: 'A', // A/B testing
featureUsage: {
reportsViewed: 25 // Behavioral targeting
}
}
}}
>Targeting Examples:
- Plan-based:
plan: 'premium'→ Show premium features - Geographic:
region: 'us-east'→ Test in specific regions - Behavioral:
reportsViewed > 10→ Target power users - Time-based:
signupDate > '2024-01-01'→ New vs. existing users
Best Practices
✅ Use the Right Hook
// Simple boolean check
const { on, loading } = useFeature('dark-mode');
// SSR-safe with default
const isDarkMode = useFeatureOn('dark-mode', false);
// Full control with status
const flag = useFlag('experiment');
if (flag.status === 'ready') { ... }
// Typed values
const maxItems = useFlagValue('max-items', 10);
// A/B test variants
const variant = useVariant('checkout-test');✅ Handle Loading States
function FeatureComponent() {
const { on, loading } = useFeature('new-feature');
if (loading) return <Skeleton />;
return on ? <NewFeature /> : <OldFeature />;
}✅ Multiple Flags
function Dashboard() {
const { on: darkMode } = useFeature('dark-mode');
const { on: newLayout } = useFeature('new-layout');
const maxItems = useFlagValue('max-items', 10);
return (
<div className={darkMode ? 'dark' : ''}>
{newLayout ? <NewLayout items={maxItems} /> : <OldLayout />}
</div>
);
}✅ Update User Context
function UpgradeButton() {
const { updateUser } = useFlags();
const handleUpgrade = () => {
// Update user context to refresh flags
updateUser({
userId: user.id,
email: user.email,
properties: { plan: 'premium' }
});
};
return <button onClick={handleUpgrade}>Upgrade</button>;
}Migration from Old API
// ❌ Old (confusing)
const { isEnabled } = useFlags();
const flag = isEnabled('my-feature');
if (flag.isReady && flag.enabled) {
return <NewFeature />;
}
// ✅ New (clear)
const { on, loading } = useFeature('my-feature');
if (loading) return <Skeleton />;
return on ? <NewFeature /> : <OldFeature />;
// Or even simpler for SSR
const isOn = useFeatureOn('my-feature', false);
return isOn ? <NewFeature /> : <OldFeature />;The old API (isEnabled) is still supported for backwards compatibility but deprecated.
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
Ready to get started? Create your first feature flag in the dashboard →
How is this guide?