Skip to main content

LaunchDarkly Feature Flags

Sanctiv uses LaunchDarkly for feature flag management. This provides:
  • Instant toggling - Enable/disable features without deployment
  • Gradual rollouts - Release to percentage of users
  • User targeting - Target by org, user, or custom attributes
  • A/B testing - Test different feature variants

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Mobile App                               │
├─────────────────────────────────────────────────────────────┤
│  LaunchDarkly SDK                                           │
│       │                                                     │
│       ├── useFeatureFlagEnabled() - Boolean flags           │
│       ├── useFeatureFlagValue() - String/multivariate       │
│       ├── checkFeatureFlag() - Outside React components     │
│       ├── trackEvent() - Custom event tracking              │
│       └── FeatureGate - Declarative feature gating          │
├─────────────────────────────────────────────────────────────┤
│  AI Agent (Cursor)                                          │
│       │                                                     │
│       └── MCP Server - Create/manage flags programmatically │
└─────────────────────────────────────────────────────────────┘

MCP Server Setup (For AI Agents)

CRITICAL: This section is for AI agents (Cursor, Claude Code) to manage flags programmatically.

Credential Types (Don’t Confuse These!)

CredentialPrefixPurposeWhere to Find
API Access Tokenapi-xxxMCP Server, REST APIAccount Settings → Authorization
Mobile Keymob-xxxClient SDK initializationProject Settings
SDK Keysdk-xxxServer SDK initializationProject Settings
⚠️ Only API Access Tokens work with the MCP server!

1. Get API Access Token

  1. Go to LaunchDarkly Dashboard
  2. Navigate to Account Settings (gear icon, top right)
  3. Go to AuthorizationAccess Tokens
  4. Click Create Token
  5. Name: cursor-mcp (or similar)
  6. Role: Writer (to create/update flags)
  7. Copy the token (starts with api-)

2. Configure MCP Server

Add to ~/.cursor/mcp.json:
{
  "mcpServers": {
    "launchdarkly": {
      "command": "npx",
      "args": [
        "-y",
        "--package", "@launchdarkly/mcp-server",
        "--", "mcp", "start",
        "--api-key", "api-YOUR-TOKEN-HERE"
      ],
      "env": {
        "NODE_EXTRA_CA_CERTS": "/etc/ssl/cert.pem"
      }
    }
  }
}

3. Verify Connectivity

After reloading Cursor, test with MCP tools:
// Use mcp_launchdarkly_list-feature-flags
{
  "projectKey": "default"
}
If you see flags returned, MCP is working!

4. Common MCP Errors

ErrorCauseSolution
”fetch failed”Network/auth issueCheck API token is valid
”Invalid account ID header”Wrong credential typeUse API Access Token, not account ID
”Tool not found”MCP not loadedReload Cursor, check mcp.json syntax
Rate limit exceededToo many API callsWait 60 seconds, retry

5. MCP Tools Available

ToolPurpose
list-feature-flagsList all flags in project
get-feature-flagGet flag details
create-feature-flagCreate new flag
update-feature-flagUpdate flag config/targeting
delete-feature-flagRemove flag
get-environmentsList environments

SDK Setup (For App Development)

1. Environment Variables

Add to .env:
# LaunchDarkly Mobile SDK Key (safe for client-side use)
EXPO_PUBLIC_LAUNCHDARKLY_MOBILE_KEY=mob-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Note: The mobile key is safe for client-side use. Never expose the SDK key (server-side only).

2. Get Your Mobile Key

  1. Go to LaunchDarkly Dashboard
  2. Navigate to Account SettingsProjects
  3. Select your project
  4. Copy the Mobile Key (NOT the SDK key)

Usage

The FeatureGate component provides declarative feature flag gating with a “Coming Soon” placeholder:
import { FeatureGate } from "@/components/FeatureGate";
import { FEATURE_FLAGS } from "@/hooks/useFeatureFlags";

// In a tab file (e.g., app/(tabs)/habits.tsx)
export default function HabitsTab() {
  return (
    <FeatureGate
      flag={FEATURE_FLAGS.HABITS_ENABLED}
      featureName="Habits"
      icon="checkbox-outline"
    >
      <HabitsScreen />
    </FeatureGate>
  );
}
When the flag is OFF, users see a friendly “Coming Soon” placeholder. When the flag is ON, the feature renders normally.

Basic Boolean Flag

import { useFeatureFlagEnabled, FEATURE_FLAGS } from "@/hooks/useFeatureFlags";

function VoiceButton() {
  const isVoiceEnabled = useFeatureFlagEnabled(FEATURE_FLAGS.VOICE_JOURNALING_ENABLED);
  
  if (!isVoiceEnabled) {
    return null;
  }
  
  return <Button>Voice Entry</Button>;
}

Multivariate Flag (A/B Testing UI Variations)

Multivariate flags return string values, enabling A/B testing of different UI approaches.

Example: Journal Prompt Style Experiment

We have a journal-prompt-style flag with three variations:
  • traditional: “What are you grateful for today?”
  • conversational: “Hey! What’s been on your heart lately?”
  • scripture-led: “Reflect on Philippians 4:8…”
import { useFeatureFlagValue, FEATURE_FLAGS, JOURNAL_PROMPT_STYLES, trackEvent, LD_EVENTS } from "@/hooks/useFeatureFlags";

function JournalPrompt() {
  // Get the assigned prompt style (LaunchDarkly handles random assignment)
  const promptStyle = useFeatureFlagValue(
    FEATURE_FLAGS.JOURNAL_PROMPT_STYLE,
    JOURNAL_PROMPT_STYLES.TRADITIONAL
  );

  // Track which variant the user saw (for analysis)
  useEffect(() => {
    trackEvent(LD_EVENTS.JOURNAL_STARTED, { prompt_style: promptStyle });
  }, []);

  // Render based on variant
  const prompts = {
    [JOURNAL_PROMPT_STYLES.TRADITIONAL]: "What are you grateful for today?",
    [JOURNAL_PROMPT_STYLES.CONVERSATIONAL]: "Hey! What's been on your heart lately?",
    [JOURNAL_PROMPT_STYLES.SCRIPTURE_LED]: "Reflect on Philippians 4:8 - What is true, noble, and praiseworthy in your life today?",
  };

  return <Text>{prompts[promptStyle]}</Text>;
}

Measuring Experiment Success

Track completion rates for each variant:
// When user completes journal entry
trackEvent(LD_EVENTS.JOURNAL_COMPLETED, {
  prompt_style: promptStyle,
  word_count: entry.text.length,
  time_to_complete: Date.now() - startTime,
});
Then analyze in LaunchDarkly (Pro plan) or export to external analytics.

Available Multivariate Flags

Flag KeyVariationsPurpose
journal-prompt-styletraditional, conversational, scripture-ledTest prompt effectiveness

Outside React Components

import { checkFeatureFlag, FEATURE_FLAGS } from "@/hooks/useFeatureFlags";

async function analyzeEntry(entry: JournalEntry) {
  const useV2 = checkFeatureFlag(FEATURE_FLAGS.AI_SUMMARY_V2);
  
  if (useV2) {
    return analyzeWithV2(entry);
  }
  return analyzeWithV1(entry);
}

Event Tracking Strategy

LaunchDarkly event tracking enables analytics, experimentation, and conversion tracking.

Standard Events

Use the LD_EVENTS constants for consistent event naming:
import { trackEvent, LD_EVENTS } from "@/hooks/useFeatureFlags";

// Journal events
trackEvent(LD_EVENTS.JOURNAL_STARTED, { entry_type: "guided" });
trackEvent(LD_EVENTS.JOURNAL_COMPLETED, { word_count: 500 });
trackEvent(LD_EVENTS.JOURNAL_ABANDONED);

// Voice events
trackEvent(LD_EVENTS.VOICE_RECORDING_STARTED, { source: "fab_menu" });
trackEvent(LD_EVENTS.VOICE_RECORDING_COMPLETED, { duration: 120 });
trackEvent(LD_EVENTS.LOCAL_TRANSCRIPTION_USED);

// AI events
trackEvent(LD_EVENTS.AI_SUMMARY_GENERATED, { model: "gpt-4" });
trackEvent(LD_EVENTS.AI_GENERATION_TIME, { model: "gpt-4" }, 2500); // metric value

// Conversion events (for experiments)
trackEvent(LD_EVENTS.FIRST_JOURNAL_COMPLETED);
trackEvent(LD_EVENTS.ONBOARDING_COMPLETED);
trackEvent(LD_EVENTS.WEEKLY_ACTIVE);

Available Event Constants

CategoryEvents
JournalJOURNAL_STARTED, JOURNAL_COMPLETED, JOURNAL_ABANDONED, JOURNAL_EDITED, JOURNAL_SEARCHED
VoiceVOICE_RECORDING_STARTED, VOICE_RECORDING_COMPLETED, VOICE_TRANSCRIPTION_COMPLETED, LOCAL_TRANSCRIPTION_USED
AIAI_SUMMARY_GENERATED, AI_GENERATION_TIME, AI_IMAGE_GENERATED, FOLLOWUP_PROMPT_SHOWN
LibraryTRUTH_SAVED, TRUTH_VIEWED, TRUTH_SHARED, TRUTH_REMINDER_SET
HabitsHABIT_CREATED, HABIT_COMPLETED, HABIT_SKIPPED, HABIT_STREAK_ACHIEVED
CompanionCOMPANION_INVITED, COMPANION_CONNECTED, COMPANION_CHAT_SENT
ConversionONBOARDING_COMPLETED, FIRST_JOURNAL_COMPLETED, WEEKLY_ACTIVE

When to Track Events

  1. Feature Usage: Track when users interact with flagged features
  2. A/B Test Metrics: Track outcomes to measure experiment success (Pro plan)
  3. Conversion Funnel: Track key milestones for product analytics
  4. Error Monitoring: Track feature failures for reliability insights

Event Tracking Best Practices

// ✅ Good - track with context
trackEvent(LD_EVENTS.JOURNAL_COMPLETED, {
  entry_type: "guided",
  word_count: 500,
  has_ai_summary: true,
  prompt_style: promptStyle,
});

// ✅ Good - track timing for experiments
const startTime = Date.now();
const summary = await generateSummary(text);
trackEvent(LD_EVENTS.AI_GENERATION_TIME, { model: "gpt-4" }, Date.now() - startTime);

// ❌ Bad - no context
trackEvent("completed");

// ❌ Bad - string literal (use constants)
trackEvent("journal-completed");

Available Flags

Complete Reference: See FEATURE_FLAGS.md for comprehensive feature flag documentation including taxonomy, user flows, and best practices.

Core Tab Flags

Flag KeyTypeDescription
insights-enabledBooleanInsights tab with analytics
habits-enabledBooleanHabit tracking feature
truth-library-enabledBooleanScripture & devotional content
companion-sharing-enabledBooleanCompanion connections

Journaling Flags

Flag KeyTypeDescription
voice-journaling-enabledBooleanVoice-to-text journaling
local-transcription-enabledBooleanOn-device Whisper transcription
journal-editing-enabledBooleanEdit existing journal entries
journal-search-enabledBooleanSearch journal entries
followup-promptsBooleanAI-generated follow-up questions

AI Flags

Flag KeyTypeDescription
ai-summary-enabledBooleanAI summary generation
ai-summary-v2BooleanNew AI summary model (experiment)
ai-image-generationBooleanGrok-powered image generation
ai-scripture-suggestions-enabledBooleanAI scripture suggestions
emotion-detection-enabledBooleanAI emotion detection
truth-generation-enabledBooleanAI truth generation from journals

Notification Flags

Flag KeyTypeDescription
push-notificationsBooleanPush notification delivery
followup-notifications-enabledBooleanFollow-up notification scheduling
truth-reminders-enabledBooleanDaily truth reminders
habit-reminders-enabledBooleanHabit reminder notifications

Social Flags

Flag KeyTypeDescription
companion-chatBooleanReal-time chat between companions

Admin Flags

Flag KeyTypeDescription
admin-panel-enabledBooleanAdmin panel access
impersonation-enabledBooleanUser impersonation
debug-modeBooleanDebug panel visibility
beta-features-enabledBooleanOpt-in beta features

Analytics Flags

Flag KeyTypeDescription
mood-trends-enabledBooleanMood trends visualization
journal-streaks-enabledBooleanJournal streak tracking
habit-streaks-enabledBooleanHabit streak tracking

User Targeting

LaunchDarkly identifies users for sophisticated targeting. The more attributes you provide, the more powerful your targeting becomes.

Basic Identification (On Login)

import { identifyUser } from "@/lib/launchdarkly";

// Basic identification
await identifyUser(user.id, {
  email: user.email,
  orgId: user.organization_id,
});

Rich Identification (Elite Targeting)

import { identifyUser, LDUserAttributes } from "@/lib/launchdarkly";
import { Platform } from "react-native";
import * as Device from "expo-device";

// Rich identification for advanced targeting
await identifyUser(user.id, {
  // Identity
  email: user.email,
  name: user.full_name,

  // Organization targeting (church-by-church rollouts)
  orgId: user.organization_id,
  orgName: "First Baptist Church",

  // Role targeting (admin-only features)
  role: "admin",
  isPastor: true,

  // Device targeting (platform-specific rollouts)
  platform: Platform.OS,
  appVersion: "1.2.0",
  deviceModel: Device.modelName,

  // Subscription targeting (premium features)
  subscriptionTier: "pro",

  // Beta targeting (early adopters)
  isBetaTester: true,
  betaEnrolledDate: "2025-12-01",
});

Reset on Logout

import { resetUser } from "@/lib/launchdarkly";

await resetUser();

Segments (Reusable User Groups)

LaunchDarkly Segments are reusable user groups that can be targeted across multiple flags. Instead of duplicating rules on every flag, create segments once and reference them. Create these segments in LaunchDarkly DashboardSegments:
Segment KeyNameRulePurpose
beta-testersBeta Testersis_beta_tester = trueUsers in beta program
pilot-churchesPilot Churchesorg_id in [uuid1, uuid2, ...]Initial pilot partners
internal-teamInternal Teamemail ends with @sanctiv.aiTeam members

Creating a Segment

  1. Go to SegmentsCreate Segment
  2. Configure:
    • Name: Beta Testers
    • Key: beta-testers
    • Description: Users who have opted into the beta program
  3. Add Rule:
    • Attribute: is_beta_tester
    • Operator: is one of
    • Values: true
  4. Click Save Segment

Using Segments in Flag Targeting

In Dashboard:
  1. Open any feature flag
  2. Add targeting rule: “User is in segment”
  3. Select beta-testers
  4. Set variation: true
Benefit: When you add users to the beta-testers segment, they automatically get access to ALL flags that target that segment.

Segment vs Direct Attribute Targeting

ApproachProsCons
SegmentReusable, centralized managementRequires dashboard setup
Direct AttributeQuick, no setup neededDuplicated rules across flags
Recommendation: Use segments for cohorts that will be targeted across multiple flags (beta testers, pilot churches).

Elite Targeting Patterns

1. Church-by-Church Pilot Rollout

Roll out Voice Journaling to pilot churches one at a time: LaunchDarkly Dashboard Setup:
  1. Open flag voice-journaling-enabled
  2. Add targeting rule:
    • If org_id is one of ["church-a-uuid", "church-b-uuid"]true
    • Default → false
Result: Only Church A and Church B users see Voice Journaling.

2. Percentage-Based Gradual Rollout

Release AI Summary V2 to 10% of users, then gradually increase: LaunchDarkly Dashboard Setup:
  1. Open flag ai-summary-v2
  2. Set fallthrough to percentage rollout:
    • Day 1: 10%
    • Day 3: 25%
    • Day 7: 50%
    • Day 14: 100%
Result: Feature gradually rolls out while you monitor metrics.

3. Beta Tester Program

Let specific users opt-in to beta features: LaunchDarkly Dashboard Setup:
  1. Open flag beta-features-enabled
  2. Add targeting rule:
    • If is_beta_tester = truetrue
    • Default → false
Code:
// Identify user as beta tester
await identifyUser(userId, {
  isBetaTester: user.has_opted_into_beta,
});

4. Role-Based Feature Access

Enable Admin Panel only for admins and pastors: LaunchDarkly Dashboard Setup:
  1. Open flag admin-panel-enabled
  2. Add targeting rules:
    • If role = admintrue
    • If is_pastor = truetrue
    • Default → false

5. Device-Specific Rollouts

Roll out to iOS first, then Android: LaunchDarkly Dashboard Setup:
  1. Open flag voice-journaling-enabled
  2. Add targeting rules:
    • If platform = iostrue
    • If platform = androidfalse (then enable later)

6. Emergency Kill Switch

Instantly disable AI features if costs spike: LaunchDarkly Dashboard:
  1. Toggle ai-summary-enabled to OFF
  2. All AI summaries stop immediately across all users

Creating New Flags

1. Add to FEATURE_FLAGS Constant

// src/hooks/useFeatureFlags.ts
export const FEATURE_FLAGS = {
  // ... existing flags
  MY_NEW_FEATURE: "my-new-feature",
} as const;

2. Create in LaunchDarkly

Option A: Via MCP (Agents)
// Use mcp_launchdarkly_create-feature-flag
{
  "projectKey": "default",
  "FeatureFlagBody": {
    "key": "my-new-feature",
    "name": "My New Feature",
    "description": "Description of what this flag controls",
    "temporary": false,
    "tags": ["mobile"]
  }
}
Option B: Via Dashboard (Manual)
  1. Go to Feature FlagsCreate Flag
  2. Name: my-new-feature
  3. Kind: Boolean (or Multivariate)
  4. Default: Off
  5. Save

3. Enable the Flag

Option A: Via MCP (Agents)
// Use mcp_launchdarkly_update-feature-flag
{
  "projectKey": "default",
  "featureFlagKey": "my-new-feature",
  "PatchWithComment": {
    "comment": "Enable for all users",
    "patch": [
      { "op": "replace", "path": "/environments/test/on", "value": true },
      { "op": "replace", "path": "/environments/production/on", "value": true }
    ]
  }
}
Option B: Via Dashboard
  1. Open flag
  2. Toggle ON for desired environments
  3. Save

4. Wire Up in Code

// Simple conditional
const isEnabled = useFeatureFlagEnabled(FEATURE_FLAGS.MY_NEW_FEATURE);

// Or use FeatureGate for screens
<FeatureGate flag={FEATURE_FLAGS.MY_NEW_FEATURE} featureName="My Feature">
  <MyFeatureScreen />
</FeatureGate>

Best Practices

1. Define Flags in FEATURE_FLAGS Constant

// ✅ Good - type-safe
const isEnabled = useFeatureFlagEnabled(FEATURE_FLAGS.VOICE_JOURNALING_ENABLED);

// ❌ Bad - prone to typos
const isEnabled = useFeatureFlagEnabled("voice-journeling-enabled");

2. Use FeatureGate for Screens

// ✅ Good - declarative, shows placeholder
<FeatureGate flag={FEATURE_FLAGS.NEW_TAB} featureName="New Tab">
  <NewTabScreen />
</FeatureGate>

// ⚠️ Okay for small UI elements
{isEnabled && <SmallButton />}

3. Track Events for Observability

// ✅ Good - track SDK connectivity on init
ldClient.track("sdk-initialized", { source: "cursor" });

// ✅ Good - track feature usage
trackEvent("voice_recording_completed", { duration: 45 });

4. Handle Loading States

function FeatureComponent() {
  const { isReady } = useFeatureFlags();
  const isEnabled = useFeatureFlagEnabled(FEATURE_FLAGS.NEW_FEATURE);
  
  if (!isReady) {
    return <LoadingSpinner />;
  }
  
  if (!isEnabled) {
    return null;
  }
  
  return <NewFeature />;
}

Troubleshooting

Flags Return Default Values Initially (UI Flash)

Symptom: Components briefly render with default flag values before switching to actual values, causing a “Coming Soon” flash. Cause: LaunchDarkly SDK initializes asynchronously. Until initialization completes, flags return default values. Solutions:
  1. Use FeatureGate component for screens - It automatically shows a loading state:
// ✅ FeatureGate handles loading state automatically
<FeatureGate flag={FEATURE_FLAGS.MY_FEATURE} featureName="My Feature">
  <MyFeatureScreen />
</FeatureGate>
  1. Check isReady from useFeatureFlags() hook before rendering:
function MyFeature() {
  const { isReady } = useFeatureFlags();
  const isEnabled = useFeatureFlagEnabled(FEATURE_FLAGS.MY_FEATURE);
  
  // Show loading while SDK initializes
  if (!isReady) {
    return <LoadingSpinner />;
  }
  
  if (!isEnabled) {
    return null;
  }
  
  return <MyFeatureContent />;
}
  1. For small UI elements, consider deferring render until ready:
const { isReady } = useFeatureFlags();
const isEnabled = useFeatureFlagEnabled(FEATURE_FLAGS.MY_BUTTON);

// Only show button after SDK is ready
{isReady && isEnabled && <MyButton />}

Flags Always Return Default

  1. Check EXPO_PUBLIC_LAUNCHDARKLY_MOBILE_KEY is set
  2. Verify it’s a Mobile Key (starts with mob-)
  3. Check LaunchDarkly dashboard for flag status
  4. Verify flag is enabled in the correct environment
  5. Check console for dev warning: [FeatureFlags] Flag "xxx" returned default value

Flag Changes Not Reflected

  1. LaunchDarkly streams updates in real-time
  2. Kill and restart the app if using dev client
  3. Check network connectivity
  4. Clear Metro cache: npx expo start --clear

User Targeting Not Working

  1. Verify identifyLDUser() is called after login
  2. Check user attributes match targeting rules
  3. Verify flag is enabled in dashboard

MCP Not Working (Agents)

  1. Verify API Access Token (not mobile key or account ID)
  2. Token should start with api-
  3. Reload Cursor after changing mcp.json
  4. Check mcp.json syntax is valid JSON
  5. Try running MCP server manually to see errors