Feature Flags

Features Documentation #

This section contains comprehensive documentation for the BonsAI feature management system.

Overview #

The Early Access Feature Flags system replaces the previous domain-based approach with a scalable, permission-based system that provides:

  • Configuration-driven feature definitions in YAML
  • Type-safe TypeScript integration with auto-generation
  • Permission-based access control
  • User opt-in requirements for all features
  • Four stability levels: early, preview, ready, and canary
  • Analytics integration for tracking adoption

canary flags are internal-only and cannot be self-served by customers. They used to be toggled by hand in the Clerk dashboard; they are now managed through the Sales Portal (see the Sales Portal docs section) Feature Flags tab, which audits every change to Datadog.

Three different “features” — do not conflate them: BonsAI feature flags (this doc) are config/features.yaml → org publicMetadata.features; Clerk RBAC Features are permission namespaces like sales_portal used to build org:<feature>:<permission> keys; Clerk Billing features are unused (tofu bills via Stripe).

Quick Start #

  1. Define feature in /config/features.yaml
  2. Run mise run codegen-feature-flags and commit the generated files (Clerk sync runs on deploy)
  3. Use isFeatureEnabled('feature_name') in your code
  4. Enable feature in Organization Settings UI
  5. Optionally announce it in the “New feature” dialog — see Announcing Features

For detailed instructions, see the full documentation.

Architecture #

Components #

  1. Configuration File (/config/features.yaml)

    • Single source of truth for all feature definitions
    • Validated against JSON schema
    • Three stability levels: early, preview, ready
  2. Clerk Storage

    • Organization metadata stores boolean flags
    • User metadata stores feature preferences
    • Simple key-value pairs for efficiency
  3. Sync Tool (tools/feature-flags/src/bin/sync_feature_flags.rs)

    • Rust binary that syncs YAML to Clerk
    • Preserves existing feature states
    • Removes obsolete features automatically
  4. Frontend Integration

    • TypeScript definitions generated from YAML
    • Legend-state for reactive feature management
    • Permission-based UI components
    • Visual status indicators with color-coded badges and icons

Adding New Features #

Step 1: Define the Feature #

Edit /config/features.yaml:

# Choose the appropriate stability level
early:
  - name: my_new_feature
    title: "My New Feature"
    description: "Brief description of what this feature does"
    warningOverride: "Optional custom warning message" # If not provided, uses default for status

preview:
  - name: another_feature
    title: "Another Feature"
    description: "A feature in preview stage"

ready:
  - name: stable_feature
    title: "Stable Feature"
    description: "A production-ready feature"

Feature naming conventions:

  • Use snake_case
  • Be descriptive but concise
  • Use consistent naming patterns (e.g., _integration suffix for integrations)

Step 2: Generate TypeScript Definitions #

# Manual generation
mise run codegen-feature-flags

# Or start dev server (auto-generates)
mise run dev

This creates/updates (commit all of them):

  • /libs/python/bonsai-model/bonsai_model/feature_flags.py
  • /apps/webapp/src/shared/lib/features/definitions.ts
  • /apps/sales-portal/src/shared/lib/features/definitions.ts
  • /apps/tofu-web/src/flags/definitions.ts
  • Type-safe FeatureName union type
  • Feature helper functions

Each app generates its own copy from the same config/features.yaml. To regenerate just one, run its target: mise run webapp-codegen-feature-flags, mise run sales-portal-codegen-feature-flags or mise run tofu-web-codegen-feature-flags.

Step 3: Sync to Organizations #

# Development (single org)
mise run sync-feature-flags
# Will prompt for org ID in dev_local environment

# Production (all orgs) - happens automatically on deploy
# Or manually:
doppler run --config prod -- cargo run --bin sync_feature_flags

Step 4: Apply in Code #

Frontend Usage #

import { isFeatureEnabled } from "@/shared/sync/features";

function MyComponent() {
  const { isFeatureEnabled } = usePermissions();

  if (isFeatureEnabled("my_new_feature")) {
    return <NewFeatureUI />;
  }

  return <StandardUI />;
}

Backend Usage (if needed) #

Currently, feature flags are frontend-only. For backend feature flags:

  1. Pass feature state from frontend in API requests
  2. Or implement backend feature checking (not yet available)

Removing Features #

Step 1: Remove from Configuration #

Delete the feature entry from /config/features.yaml.

Step 2: Update Code #

  1. Search for feature usage:
grep -r "my_old_feature" apps/webapp/src
  1. Remove or replace feature checks with appropriate logic

  2. Regenerate types:

mise run codegen-feature-flags

Step 3: Sync Removal #

# The sync tool automatically removes features not in YAML
mise run sync-feature-flags

The sync tool will:

  • Remove the feature from all organization metadata
  • Log which features are being removed
  • Preserve all other feature states

Code Generation #

Automatic Generation #

During development, types are auto-generated when:

  • Running mise run dev (watches for changes)
  • Running mise run webapp-build
  • Running mise run sync-feature-flags

Manual Generation #

mise run codegen-feature-flags

What Gets Generated #

  1. Feature Definitions (FEATURE_DEFINITIONS)

    • Validated feature configuration
    • Runtime type checking with Zod
  2. Type Definitions

    • FeatureName union type (e.g., 'dropbox_integration' | 'onedrive_sync' | ...)
    • FeatureStatus type ('early' | 'preview' | 'ready')
    • Feature interface with name, title, description, status, and warningOverride
  3. Helper Functions

    • getFeatureDefinitions()
    • getFeatureByName()
    • getDefaultWarningForStatus()

Sync Behavior #

How Sync Works #

  1. Reads YAML Configuration

    • Validates against JSON schema
    • Collects all feature names
  2. Fetches Organizations

    • Paginates through all orgs (100 at a time)
    • Or single org in development
  3. Updates Metadata

    • Preserves existing feature values
    • Adds new features as false
    • Removes obsolete features
    • Only updates if changes detected
  4. Rate Limiting

    • 150ms delay between org updates
    • Exponential backoff on failures
    • Max 3 retries per organization

Sync Schedule #

  • Automatic: On every deployment (dev and prod)
  • Manual: Via mise tasks
  • Local Dev: Single org sync for safety

Sync Output #

✓ Schema validation passed
Processing organizations 1 to 100
✓ Updated features for org: Acme Corp
→ No changes needed for org: org_xyz123
→ Removing features: ["old_feature_1", "old_feature_2"]

Sync complete: 95 successful, 0 errors

Deployment Process #

Development Deployment #

  1. Code Changes

    • Push changes to branch
    • GitHub Actions runs on push
  2. Build Process

    - name: Generate Feature Types
      run: mise run codegen-feature-flags
    
    - name: Build Applications
      run: mise run build
    
  3. Deployment

    - name: Deploy to Dev
      run: mise run deploy-dev
    
    - name: Sync Feature Flags
      env:
        CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
      run: cargo run --bin sync_feature_flags
    

Production Deployment #

Same as development but:

  • Requires PR approval
  • Runs on merge to main
  • Syncs to all production organizations

Environment Variables #

Required for sync tool:

  • CLERK_SECRET_KEY: Clerk API key
  • TEST_ORG_ID: (Optional) Single org for testing

Testing Features #

Local Development #

  1. Set Test Organization

    export TEST_ORG_ID=org_your_test_org_id
    mise run sync-feature-flags
    
  2. Enable Feature in UI

    • Navigate to Organization Settings
    • Find “Feature Flags” section
    • Toggle desired feature
  3. Test Feature

    • Verify feature appears/works as expected
    • Check warning dialogs for early/preview features
    • Test enable/disable behavior

Troubleshooting #

Common Issues #

Feature Not Appearing #

  1. Check YAML Configuration

    cat config/features.yaml | grep my_feature
    
  2. Verify Sync Completed

    # Check org metadata in Clerk dashboard
    
  3. Verify Permissions

    • User needs org:feature:update permission
    • Check in Organization Settings > Roles

Best Practices #

Feature Naming #

  • Use descriptive names that indicate what the feature does
  • Group related features with common prefixes
  • Examples:
    • ai_document_extraction (AI features)
    • dropbox_integration (Integration features)
    • batch_operations (Operation features)

Status Guidelines #

Early #

  • Features in early development
  • New ideas and concepts
  • May have significant bugs or change drastically
  • Data loss is possible and bugs are expected
  • No support provided - use at users’ own risk

Preview #

  • Features being refined for wider release
  • More stable than early features but not fully supported
  • Basic functionality works but may undergo changes
  • Try with caution

Ready #

  • Production-ready features
  • Released and fully supported
  • Backward compatibility maintained
  • Safe for production use
  • Still polishing for GA

Warning Messages #

  • Be specific about risks
  • Include mitigation steps if possible
    • Update warnings as feature stabilizes
    • Remove warnings when promoting to ready status

Migration Strategy #

When moving features between statuses:

  1. Early → Preview

    • Update status in YAML from early to preview
    • Adjust warning message to be less severe
    • Document known issues and improvements made
  2. Preview → Ready

    • Move from preview to ready section
    • Remove warning override (ready features have no warnings)
    • Update documentation to reflect stable status
    • Ensure full testing and support processes are in place

Security Considerations #

  1. Permission Checks

    • Always verify org:feature:update permission
    • Don’t expose internal feature names to unauthorized users
  2. Data Validation

    • Validate feature names against known list
    • Sanitize user inputs
    • Use TypeScript types for safety

UI Components and Visual Design #

Status Badges #

Each feature status is visually represented with distinct badges:

Ready Status #

  • Color: Teal (teal-500 background, white text)
  • Icon: Check mark (✓)
  • Tooltip: “Released and supported.”
  • Meaning: Fully stable, production-ready features

Preview Status #

  • Color: Blue (blue-500 background, white text)
  • Icon: Sparkles (✨)
  • Tooltip: “Try what’s coming next.”
  • Meaning: Features being refined for wider release

Early Status #

  • Color: Violet (violet-500 background, white text)
  • Icon: Sprout (🌱)
  • Tooltip: “Help test a new idea.”
  • Meaning: Experimental features for testing new concepts

Default Warning Messages #

The system provides default warning messages for non-ready features:

Early Features #

This feature is early and not yet fully defined. It may change drastically without notice, data loss is possible, and bugs are expected. By enabling this feature, you acknowledge that you use it at your own risk and support will not be provided for any issues that arise.

Preview Features #

This feature is in preview. While more refined than early features, it may still undergo changes and is not fully supported. Use with caution and report any issues you encounter.

Ready Features #

  • No warning message (undefined)
  • Features are considered stable and fully supported

Feature Toggle UI #

Features are displayed in the Organization Settings with:

  • Feature title and description
  • Status badge with appropriate color and icon
  • Toggle switch for enable/disable
  • Warning dialogs for early/preview features when enabling

Announcing Features (“New feature” dialog) #

On app load the webapp shows a dialog, one card per feature, for announced features the user has not seen yet (ENG-7218). Every early/preview/ready flag is announced unless it sets announce: false; all copy comes from config/features.yaml. Cloudflare R2 only holds optional media and the Help Center link.

early:
  - name: auto_page_classification
    title: "Auto Page Classification"
    description: "We automatically detect and label each page's document type as it comes in."

ready:
  - name: integration_moneyforward
    title: "MoneyForward Cloud"
    description: "Enable MoneyForward Cloud accounting integration for Japanese market."
    announce: false # opt out: not news to existing users

# Shipped features with no setting (or flags that went GA). Not synced to Clerk and
# not shown in Settings → Features.
general_available:
  - name: drag_drop_upload
    title: "Drop files anywhere to upload"
    description: "Drag files onto any page in tofu and we'll upload them."
    announceOrder: 1 # optional: shown before every other card, across sections
Source Who sees the card Badge
general_available entry Everyone Ready (no toggle, “Got it”)
ready flag Everyone Ready
early / preview flag Orgs that chose “Get notified when new features arrive” at onboarding (notify_on_new_features) Early / Preview
announce: false or canary Never –
  • Cards with announceOrder (integer ≥ 1) show first, lowest first, whatever their section; equal values keep features.yaml order. The rest follow in features.yaml order: flags (early → preview → ready), then general_available. Moving an entry within its section also moves it in Settings → Features.
  • Only users with org:feature:update get an interactive switch; everyone else sees a read-only ON/OFF.
  • A card is marked seen the first time it is shown, in the user’s Clerk unsafeMetadata.seenFeatureAnnouncements ({ "whatsapp": true, ... }; set a key to false or delete it to show that card again). Each write drops keys that are no longer announced, so the map never outgrows the active announcements.
  • Who enabled a flag (“Enabled by … · 2 days ago”) is written to the org’s publicMetadata.featureToggledBy by the toggle API; the sync tool prunes entries of removed flags.

Lifecycle #

  1. Early / preview: add the flag; opted-in orgs get the card.
  2. Ready: move the flag to ready; orgs that did not opt in now get the card too.
  3. GA: when the flag is deleted, move its entry to general_available in the same PR if it should still be announced (drop announce and warningOverride), or just delete it.
  4. No setting at all: add an entry to general_available.
  5. Stop announcing: set announce: false / remove the general_available entry, ideally 4–6 weeks after release.

One key per feature: a user who saw the card as early does not get it again at ready or GA. To re-announce deliberately, use a new name.

Optional per feature: features/{name}/assets.json in the assets-webapp R2 bucket (served at https://assets.gotofu.com), with its media files in the same folder (Cloudflare dashboard). Send the engineer who had access the files to upload. Keep a copy of assets.json in config/feature-announcements/{name}/assets.json so it is reviewed in the PR. Schema: config/feature-announcements/assets.schema.json (URLs and file names only — never copy). A missing file gives a text-only card; an invalid one is logged and also renders text-only.

{
  "$schema": "https://assets.gotofu.com/features/assets.schema.json",
  "media": { "type": "youtube", "url": "https://youtu.be/dQw4w9WgXcQ", "startSeconds": 12 },
  "helpCenterUrl": "https://help.gotofu.com/en/articles/auto-page-classification"
}
  • media.type: "media" — src is a file in the folder: png/jpg/webp/gif/svg render as an image, mp4/webm as a muted looping video. fallback (image) is the video’s poster until it plays.
  • media.type: "youtube" — an embedded YouTube player that starts muted 0.5s after the card opens. Until then a play button shows over fallback (or over a blank frame without it); clicking it first plays with sound.

Upload before the features.yaml PR ships (cards are marked seen on first display, so early viewers would never see media added later). Media first, then assets.json, each with the right content type (image/png, image/jpeg, image/webp, image/gif, image/svg+xml, video/mp4, video/webm, application/json): Safari will not play an mp4 served with the wrong type.

  • Media is cached for a year — never overwrite a file; upload demo-v2.mp4 and point src at it. assets.json is cached for 5 minutes (plus 5 minutes in the webapp’s route cache).