Claude-skill-registry feature-flags-implementation

Database-backed feature flag systems in the Orient. Use when implementing feature flags, debugging flag-related issues, or understanding the flag hierarchy. Covers schema design, API patterns, frontend hooks, ID format conversion, and ProtectedRoute integration.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/feature-flags-implementation" ~/.claude/skills/majiayu000-claude-skill-registry-feature-flags-implementation && rm -rf "$T"
manifest: skills/data/feature-flags-implementation/SKILL.md
source content

Feature Flags Implementation

Quick Reference

# Check feature flags in database
sqlite3 "$SQLITE_DB_PATH" "SELECT id, name, enabled FROM feature_flags ORDER BY sort_order;"

# Test feature flag API
curl -X GET http://localhost:4098/api/feature-flags \
  -H "Authorization: Bearer $(cat ~/.dashboard_token)"

Architecture Overview

┌─────────────────────┐     ┌─────────────────────┐     ┌─────────────────────┐
│   Database          │────▶│   Dashboard API     │────▶│   Frontend          │
│   (SQLite)          │     │   (Express)         │     │   (React)           │
│                     │     │                     │     │                     │
│ feature_flags table │     │ /api/feature-flags  │     │ useFeatureFlags()   │
│ snake_case IDs      │     │ JWT Authentication  │     │ camelCase IDs       │
│ dot.notation        │     │                     │     │ underscore_notation │
└─────────────────────┘     └─────────────────────┘     └─────────────────────┘

Database Schema

Feature Flags Table

Located in

packages/database/src/schema/index.ts
:

export const featureFlags = sqliteTable('feature_flags', {
  id: text('id').primaryKey(), // e.g., 'mini_apps', 'operations.billing'
  name: text('name').notNull(), // Display name
  description: text('description'),
  enabled: integer('enabled', { mode: 'boolean' }).default(true),
  category: text('category').default('ui'),
  sortOrder: integer('sort_order').default(0),
  createdAt: integer('created_at', { mode: 'timestamp' }),
  updatedAt: integer('updated_at', { mode: 'timestamp' }),
});

ID Naming Convention

Database uses snake_case with dot notation for hierarchy:

mini_apps                           # Root flag
mini_apps.create                    # Child of mini_apps
mini_apps.edit_with_ai              # Child with multi-word name
operations                          # Root flag
operations.billing                  # Child of operations
operations.monitoring               # Child of operations
operations.monitoring.server_health # Nested child (grandchild)

API Endpoints

Located in

packages/dashboard/src/server/routes/featureFlags.routes.ts
:

MethodEndpointDescription
GET
/api/feature-flags
Get all flags with effective values
GET
/api/feature-flags/list
Get detailed flag list
GET
/api/feature-flags/effective
Get flat map of flag IDs to booleans
PUT
/api/feature-flags/:flagId
Update a flag
PUT
/api/feature-flags/:flagId/override
Set user override
DELETE
/api/feature-flags/:flagId/override
Remove user override

API Response Format

// GET /api/feature-flags returns:
{
  flags: [
    {
      id: 'mini_apps',
      name: 'Mini-Apps',
      description: 'Enable mini-apps feature',
      enabled: false,
      category: 'ui',
      sortOrder: 0,
      createdAt: '2024-01-15T10:00:00.000Z',
      updatedAt: '2024-01-15T10:00:00.000Z',
      userOverride: null,
      effectiveValue: false,
    },
    // ... more flags
  ];
}

Frontend Implementation

ID Format Conversion

Database → Frontend:

snake_case.dot
camelCase_underscore

// In useFeatureFlags.ts
// API returns: { id: 'mini_apps.edit_with_ai', ... }
// Frontend expects: 'miniApps_editWithAi'

const camelId = flag.id.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
// 'mini_apps.edit_with_ai' → 'miniApps.editWithAi'

const uiId = camelId.replace(/\./g, '_');
// 'miniApps.editWithAi' → 'miniApps_editWithAi'

Frontend → Database:

camelCase_underscore
snake_case.dot

// In FeatureFlagsPage.tsx
const toDbId = (uiId: string): string => {
  const parts = uiId.split('_');
  const dbParts = parts.map((part) =>
    part
      .replace(/([A-Z])/g, '_$1')
      .toLowerCase()
      .replace(/^_/, '')
  );
  return dbParts.join('.');
};

// 'miniApps_editWithAi' → 'mini_apps.edit_with_ai'

useFeatureFlags Hook

Located in

packages/dashboard-frontend/src/hooks/useFeatureFlags.ts
:

import { getFeatureFlags } from '../api'; // MUST use authenticated API

export function useFeatureFlags(): UseFeatureFlagsReturn {
  const [flags, setFlags] = useState<Record<string, FeatureFlagDefinition>>(PRE_LAUNCH_DEFAULTS);

  const loadFlags = useCallback(async () => {
    try {
      // Use authenticated API function, NOT raw fetch
      const data = await getFeatureFlags();

      // Transform API format to UI format
      const flagsFromApi: Record<string, FeatureFlagDefinition> = {};
      for (const flag of data.flags) {
        const uiId = convertToUiId(flag.id);
        flagsFromApi[uiId] = {
          enabled: flag.effectiveValue ?? flag.enabled ?? false,
          uiStrategy: 'hide',
          parentFlag: extractParentFlag(flag.id),
        };
      }

      setFlags({ ...PRE_LAUNCH_DEFAULTS, ...flagsFromApi });
    } catch (err) {
      // Fallback to defaults on error
      setFlags(PRE_LAUNCH_DEFAULTS);
    }
  }, []);

  // ... isEnabled, shouldHide, shouldNotify helpers
}

PRE_LAUNCH_DEFAULTS

Safe defaults when API is unavailable:

const PRE_LAUNCH_DEFAULTS: Record<string, FeatureFlagDefinition> = {
  // Disabled by default (new/experimental features)
  miniApps: { enabled: false, uiStrategy: 'hide', route: '/apps' },
  miniApps_create: { enabled: false, parentFlag: 'miniApps', uiStrategy: 'hide' },

  // Enabled by default (core features)
  agentRegistry: { enabled: true, uiStrategy: 'hide', route: '/agents' },
  operations: { enabled: true, uiStrategy: 'hide', route: '/operations' },
  operations_billing: { enabled: true, parentFlag: 'operations', uiStrategy: 'hide' },
};

FeatureFlagsPage Component

Located in

packages/dashboard-frontend/src/components/Settings/FeatureFlagsPage.tsx
:

import { setFeatureFlagOverride } from '../../api'; // MUST use authenticated API

const updateFlag = async (flagId: string, updates: Partial<FeatureFlagDefinition>) => {
  try {
    const dbId = toDbId(flagId); // Convert UI ID to database ID
    await setFeatureFlagOverride(dbId, updates.enabled ?? false);
    await refresh(); // Reload flags
  } catch (error) {
    alert('Failed to update feature flag');
  }
};

ProtectedRoute Integration

Located in

packages/dashboard-frontend/src/components/ProtectedRoute.tsx
:

function ProtectedRoute({ flagId, children }: Props) {
  const { isEnabled, shouldHide, shouldNotify } = useFeatureFlags();

  if (shouldHide(flagId)) {
    return <Navigate to="/" replace />;
  }

  if (shouldNotify(flagId)) {
    return <FeatureDisabledOverlay />;
  }

  return children;
}

// Usage in routes
<Route
  path="/apps"
  element={
    <ProtectedRoute flagId="miniApps">
      <AppsPage />
    </ProtectedRoute>
  }
/>

Database Migrations

Adding a New Feature Flag

Create migration in

data/migrations/
:

-- XXXX_add_my_feature_flag.sql
INSERT INTO feature_flags (id, name, description, enabled, category, sort_order)
VALUES
  ('my_feature', 'My Feature', 'Description of feature', false, 'ui', 100),
  ('my_feature.sub_feature', 'Sub Feature', 'A sub-feature', false, 'ui', 101)
ON CONFLICT (id) DO NOTHING;

Adding Child Flags

-- Add child flags under existing parent
INSERT INTO feature_flags (id, name, description, enabled, category, sort_order)
VALUES
  ('operations.new_child', 'New Child', 'Under operations', true, 'ui', 50)
ON CONFLICT (id) DO NOTHING;

Common Issues

Issue: 401 Unauthorized on Flag Updates

Cause: Using raw

fetch()
instead of authenticated API functions.

Fix: Import and use functions from

api.ts
:

// BAD
const response = await fetch(`/api/feature-flags/${id}`, {
  method: 'PUT',
  credentials: 'include',
  body: JSON.stringify({ enabled }),
});

// GOOD
import { setFeatureFlagOverride } from '../api';
await setFeatureFlagOverride(id, enabled);

Issue: Flag Shows in UI but Not in Database

Cause: Flag is defined in PRE_LAUNCH_DEFAULTS but missing from database.

Fix: Add migration to create the flag in database, or remove from defaults if not needed.

Issue: Child Flag Accessible When Parent Disabled

Cause:

isEnabled
doesn't check parent chain, or parent flag ID is wrong.

Fix: Ensure

parentFlag
is set correctly in both database parent extraction and defaults:

// The isEnabled function checks parent chain
const isEnabled = (flagId: string): boolean => {
  const flag = flags[flagId];
  if (!flag?.enabled) return false;
  if (flag.parentFlag) {
    return isEnabled(flag.parentFlag); // Recursive check
  }
  return true;
};

Issue: ID Conversion Mismatch

Symptoms: Flag exists in DB but not showing in UI, or updates go to wrong flag.

Debug:

console.log('UI ID:', uiId);
console.log('DB ID:', toDbId(uiId));
// Verify they match expected values

Checklist for New Feature Flags

  • Add flag to database migration
  • Run migration:
    ./run.sh dev
    (auto-runs migrations)
  • Add to PRE_LAUNCH_DEFAULTS in
    useFeatureFlags.ts
    (if needs default)
  • Export from
    @orientbot/database
    if queried server-side
  • Add ProtectedRoute wrapper if flag controls route access
  • Update navigation to conditionally show/hide based on flag
  • Test toggle in UI persists to database
  • Test parent/child relationships work correctly

Testing Feature Flags

Manual Testing

  1. Navigate to Settings → Feature Flags
  2. Toggle a flag
  3. Verify database updated:
    sqlite3 "$SQLITE_DB_PATH" "SELECT id, enabled FROM feature_flags WHERE id = 'mini_apps';"
    
  4. Verify UI reflects change (navigation item appears/disappears)

Automated Testing

// In a test file
import { describe, it, expect, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import { useFeatureFlags } from '../hooks/useFeatureFlags';

describe('useFeatureFlags', () => {
  it('should return defaults when API fails', async () => {
    vi.mock('../api', () => ({
      getFeatureFlags: vi.fn().mockRejectedValue(new Error('Network error')),
    }));

    const { result } = renderHook(() => useFeatureFlags());

    // Should use PRE_LAUNCH_DEFAULTS
    expect(result.current.isEnabled('miniApps')).toBe(false);
    expect(result.current.isEnabled('agentRegistry')).toBe(true);
  });
});