Feature Flags Usage Guide
This guide explains how to add new feature flags and use them correctly across frontend and backend.
Quick Start
Add a New Feature Flag in 3 Steps:
- Add flag to database seed (
src/scripts/seed-missing-flags.ts) - Add flag ID to frontend list (
src/routes/features.ts) - Use in code (
FeatureGatecomponent oruseFeatureFlaghook)
How to Add a New Feature Flag
Step 1: Add to Database Seed
Add your flag to apps/backend/src/scripts/seed-missing-flags.ts:
const featureFlags = [
{
name: 'my_new_feature',
description: 'Enable my awesome new feature',
isEnabled: false, // Start disabled for gradual rollout
enabledForTiers: [],
enabledForCompanyIds: [],
},
// ... other flags
];Field Reference:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Flag ID (snake_case) - becomes database ID |
description | string | ✅ | Human-readable description |
isEnabled | boolean | ✅ | Global kill switch (true = enabled for all) |
enabledForTiers | string[] | ❌ | Not currently used |
enabledForCompanyIds | string[] | ❌ | Company IDs to always enable |
Run the seed:
cd apps/backend
pnpm tsx src/scripts/seed-missing-flags.tsStep 2: Add to Frontend Flags List
Add your flag ID to apps/backend/src/routes/features.ts:
const FRONTEND_FLAGS = [
'approvals',
'direct_posting',
'scheduling',
'inbox',
'link_in_bio',
'scripts',
'measures',
'multi_agent_system',
'my_new_feature', // ← Add here
] as const;IMPORTANT
Only flags in FRONTEND_FLAGS are sent to the frontend via /api/features/enabled.
Step 3: Use in Frontend Code
Choose between FeatureGate component or useFeatureFlag hook.
Usage Patterns
When to Use FeatureGate
Use when you want to conditionally render entire components:
import { FeatureGate } from '@tendsocial/ui';
function MyPage() {
return (
<div>
<h1>My Page</h1>
{/* Standard usage - show when enabled */}
<FeatureGate flag="my_new_feature">
<NewAwesomeComponent />
</FeatureGate>
{/* With fallback - show old component when disabled */}
<FeatureGate flag="my_new_feature" fallback={<OldComponent />}>
<NewAwesomeComponent />
</FeatureGate>
{/* Inverted - show when disabled (for deprecation) */}
<FeatureGate flag="my_new_feature" invert>
<DeprecationBanner />
</FeatureGate>
</div>
);
}Benefits:
- Declarative and easy to read
- Automatically handles loading states
- No conditional logic needed
When to Use useFeatureFlag
Use when you need programmatic access to flag state:
import { useFeatureFlag, useFeatureFlags } from '@tendsocial/ui';
function MyComponent() {
// Single flag check
const isNewUIEnabled = useFeatureFlag('my_new_feature');
// Access all flags + loading state
const { flags, loading, isEnabled } = useFeatureFlags();
// Show loading state
if (loading) return <Spinner />;
// Conditional logic
if (isNewUIEnabled) {
return <NewUI />;
}
return <OldUI />;
}Use Cases:
- Complex conditional logic
- Multiple flag checks
- Need loading state
- Dynamic flag IDs
Naming Conventions
Follow these conventions for consistency:
Flag ID (name field in database)
- Use
snake_case - Be specific and descriptive
- Prefix with feature area if appropriate
Examples:
✅ campaign_centric_ui
✅ ai_gateway_failover
✅ user_collaboration_profiling
✅ inbox_unified_view
❌ newFeature
❌ test
❌ feature1Frontend Component Props
- Use camelCase for React props
- Match database flag ID when possible
// Flag in DB: 'my_new_feature'
<FeatureGate flag="my_new_feature"> {/* ✅ */}Backend Usage
Check Feature Access
Use featureAccessService for comprehensive checks (includes entitlements, segments, overrides):
import { featureAccessService } from '@/services/billing/featureAccessService.js';
// In route handler
async (request, reply) => {
const { userId, companyId } = request.user;
const hasAccess = await featureAccessService.hasAccess('my_new_feature', {
companyId,
userId,
email: request.user.email,
tier: request.user.tier,
});
if (!hasAccess) {
return reply.code(403).send({ error: 'Feature not available' });
}
// ... proceed with feature
}Simple Flag Check (Deprecated)
For backward compatibility only - use featureAccessService for new code:
import { isFeatureEnabled } from '@/services/billing/featureFlags.service.js';
const enabled = await isFeatureEnabled('my_feature', {
companyId: 'company-123'
});Testing Feature Flags
Unit Tests
Mock the context for isolated testing:
import { render, screen } from '@testing-library/react';
import { FeatureFlagProvider } from '@tendsocial/ui';
it('shows new feature when flag enabled', () => {
render(
<FeatureFlagProvider initialFlags={{ my_new_feature: true }}>
<MyComponent />
</FeatureFlagProvider>
);
expect(screen.getByText('New Feature')).toBeInTheDocument();
});
it('hides new feature when flag disabled', () => {
render(
<FeatureFlagProvider initialFlags={{ my_new_feature: false }}>
<MyComponent />
</FeatureFlagProvider>
);
expect(screen.queryByText('New Feature')).not.toBeInTheDocument();
});Integration Tests
Test the full flow including API:
import { vi } from 'vitest';
it('fetches flags from API', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ flags: { my_new_feature: true } }),
}));
render(
<FeatureFlagProvider>
<MyComponent />
</FeatureFlagProvider>
);
await waitFor(() => {
expect(screen.getByText('New Feature')).toBeInTheDocument();
});
});Gradual Rollout Strategy
Use this process for safe feature releases:
1. Development (Internal Testing)
{
name: 'my_new_feature',
isEnabled: false, // Global OFF
enabledForCompanyIds: ['internal-company-id'], // Only for us
}2. Beta (Selected Customers)
Enable via Platform Console:
- Navigate to Platform > Feature Flags
- Find your flag
- Add beta customer company IDs to
companyOverrides
3. General Availability
{
name: 'my_new_feature',
isEnabled: true, // Global ON for everyone
}4. Cleanup (After Flag is Stable)
Once the flag has been enabled globally for 2+ weeks with no issues:
Remove frontend usage:
tsx// Before <FeatureGate flag="my_new_feature"> <NewComponent /> </FeatureGate> // After <NewComponent />Remove from frontend list:
- Delete from
FRONTEND_FLAGSinsrc/routes/features.ts
- Delete from
Database cleanup (optional):
sqlDELETE FROM "FeatureFlag" WHERE id = 'my_new_feature';Remove from seed script:
- Delete from
seed-missing-flags.ts
- Delete from
Common Pitfalls
❌ Forgetting to Add to FRONTEND_FLAGS
Problem:
// Added to seed.ts ✅
// BUT NOT in FRONTEND_FLAGS ❌
// Result: Flag never reaches frontend!Solution: Always add flag ID to both locations.
❌ Using camelCase for Flag IDs
Problem:
{
name: 'myNewFeature', // ❌ Will cause inconsistency
}Solution: Always use snake_case for flag IDs.
❌ Not Testing Both States
Problem:
// Only tests enabled state ❌
it('shows feature', () => {
render(<FeatureGate flag="my_feature"><NewUI /></FeatureGate>);
});Solution: Test both enabled AND disabled states.
❌ Leaving Flags Forever
Problem: Flags accumulate over time, creating tech debt.
Solution: Clean up flags 2-4 weeks after 100% rollout.
Platform Console Management
Super Admins can manage flags without code changes:
- Navigate to Platform > Feature Flags
- Use the UI to:
- Toggle global enabled/disabled
- Add company overrides
- Add user overrides
- View flag usage
- Change status (development → active → deprecated)
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ Frontend │
├─────────────────────────────────────────────────────────────┤
│ FeatureGate Component ←→ FeatureFlagProvider ←→ API │
│ useFeatureFlag Hook │
└──────────────────────────┬──────────────────────────────────┘
│
│ GET /api/features/enabled
│
┌──────────────────────────▼──────────────────────────────────┐
│ Backend │
├─────────────────────────────────────────────────────────────┤
│ routes/features.ts → featureAccessService │
│ → featureFlags.service │
│ → Database (FeatureFlag table) │
└─────────────────────────────────────────────────────────────┘Data Flow:
- Frontend calls
/api/features/enabled - Backend checks
featureAccessService.hasAccess()for each flag - Service evaluates: Overrides → Segments → Global Switch
- Returns
{ flags: { flag_name: true/false } } FeatureFlagProvidermakes flags available via Context- Components use
FeatureGateoruseFeatureFlag