Skip to content

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:

  1. Add flag to database seed (src/scripts/seed-missing-flags.ts)
  2. Add flag ID to frontend list (src/routes/features.ts)
  3. Use in code (FeatureGate component or useFeatureFlag hook)

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:

typescript
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:

FieldTypeRequiredDescription
namestringFlag ID (snake_case) - becomes database ID
descriptionstringHuman-readable description
isEnabledbooleanGlobal kill switch (true = enabled for all)
enabledForTiersstring[]Not currently used
enabledForCompanyIdsstring[]Company IDs to always enable

Run the seed:

bash
cd apps/backend
pnpm tsx src/scripts/seed-missing-flags.ts

Step 2: Add to Frontend Flags List

Add your flag ID to apps/backend/src/routes/features.ts:

typescript
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:

tsx
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:

tsx
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
❌ feature1

Frontend Component Props

  • Use camelCase for React props
  • Match database flag ID when possible
tsx
// 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):

typescript
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:

typescript
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:

typescript
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:

typescript
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)

typescript
{
    name: 'my_new_feature',
    isEnabled: false,  // Global OFF
    enabledForCompanyIds: ['internal-company-id'],  // Only for us
}

2. Beta (Selected Customers)

Enable via Platform Console:

  1. Navigate to Platform > Feature Flags
  2. Find your flag
  3. Add beta customer company IDs to companyOverrides

3. General Availability

typescript
{
    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:

  1. Remove frontend usage:

    tsx
    // Before
    <FeatureGate flag="my_new_feature">
        <NewComponent />
    </FeatureGate>
    
    // After
    <NewComponent />
  2. Remove from frontend list:

    • Delete from FRONTEND_FLAGS in src/routes/features.ts
  3. Database cleanup (optional):

    sql
    DELETE FROM "FeatureFlag" WHERE id = 'my_new_feature';
  4. Remove from seed script:

    • Delete from seed-missing-flags.ts

Common Pitfalls

❌ Forgetting to Add to FRONTEND_FLAGS

Problem:

typescript
// 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:

typescript
{
    name: 'myNewFeature',  // ❌ Will cause inconsistency
}

Solution: Always use snake_case for flag IDs.

❌ Not Testing Both States

Problem:

typescript
// 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:

  1. Navigate to Platform > Feature Flags
  2. 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:

  1. Frontend calls /api/features/enabled
  2. Backend checks featureAccessService.hasAccess() for each flag
  3. Service evaluates: Overrides → Segments → Global Switch
  4. Returns { flags: { flag_name: true/false } }
  5. FeatureFlagProvider makes flags available via Context
  6. Components use FeatureGate or useFeatureFlag

See Also

TendSocial Documentation