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
canaryflags 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→ orgpublicMetadata.features; Clerk RBAC Features are permission namespaces likesales_portalused to buildorg:<feature>:<permission>keys; Clerk Billing features are unused (tofu bills via Stripe).
Quick Start #
- Define feature in
/config/features.yaml - Run
mise run codegen-feature-flagsand commit the generated files (Clerk sync runs on deploy) - Use
isFeatureEnabled('feature_name')in your code - Enable feature in Organization Settings UI
- Optionally announce it in the “New feature” dialog — see Announcing Features
For detailed instructions, see the full documentation.
Architecture #
Components #
-
Configuration File (
/config/features.yaml)- Single source of truth for all feature definitions
- Validated against JSON schema
- Three stability levels: early, preview, ready
-
Clerk Storage
- Organization metadata stores boolean flags
- User metadata stores feature preferences
- Simple key-value pairs for efficiency
-
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
-
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.,
_integrationsuffix 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
FeatureNameunion 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:
- Pass feature state from frontend in API requests
- 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 #
- Search for feature usage:
grep -r "my_old_feature" apps/webapp/src
-
Remove or replace feature checks with appropriate logic
-
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 #
-
Feature Definitions (
FEATURE_DEFINITIONS)- Validated feature configuration
- Runtime type checking with Zod
-
Type Definitions
FeatureNameunion type (e.g.,'dropbox_integration' | 'onedrive_sync' | ...)FeatureStatustype ('early' | 'preview' | 'ready')Featureinterface with name, title, description, status, and warningOverride
-
Helper Functions
getFeatureDefinitions()getFeatureByName()getDefaultWarningForStatus()
Sync Behavior #
How Sync Works #
-
Reads YAML Configuration
- Validates against JSON schema
- Collects all feature names
-
Fetches Organizations
- Paginates through all orgs (100 at a time)
- Or single org in development
-
Updates Metadata
- Preserves existing feature values
- Adds new features as
false - Removes obsolete features
- Only updates if changes detected
-
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 #
-
Code Changes
- Push changes to branch
- GitHub Actions runs on push
-
Build Process
- name: Generate Feature Types run: mise run codegen-feature-flags - name: Build Applications run: mise run build -
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 keyTEST_ORG_ID: (Optional) Single org for testing
Testing Features #
Local Development #
-
Set Test Organization
export TEST_ORG_ID=org_your_test_org_id mise run sync-feature-flags -
Enable Feature in UI
- Navigate to Organization Settings
- Find “Feature Flags” section
- Toggle desired feature
-
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 #
-
Check YAML Configuration
cat config/features.yaml | grep my_feature -
Verify Sync Completed
# Check org metadata in Clerk dashboard -
Verify Permissions
- User needs
org:feature:updatepermission - Check in Organization Settings > Roles
- User needs
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:
-
Early → Preview
- Update status in YAML from
earlytopreview - Adjust warning message to be less severe
- Document known issues and improvements made
- Update status in YAML from
-
Preview → Ready
- Move from
previewtoreadysection - Remove warning override (ready features have no warnings)
- Update documentation to reflect stable status
- Ensure full testing and support processes are in place
- Move from
Security Considerations #
-
Permission Checks
- Always verify
org:feature:updatepermission - Don’t expose internal feature names to unauthorized users
- Always verify
-
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-500background, white text) - Icon: Check mark (✓)
- Tooltip: “Released and supported.”
- Meaning: Fully stable, production-ready features
Preview Status #
- Color: Blue (
blue-500background, white text) - Icon: Sparkles (✨)
- Tooltip: “Try what’s coming next.”
- Meaning: Features being refined for wider release
Early Status #
- Color: Violet (
violet-500background, 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 keepfeatures.yamlorder. The rest follow infeatures.yamlorder: flags (early → preview → ready), thengeneral_available. Moving an entry within its section also moves it in Settings → Features. - Only users with
org:feature:updateget 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 tofalseor 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.featureToggledByby the toggle API; the sync tool prunes entries of removed flags.
Lifecycle #
- Early / preview: add the flag; opted-in orgs get the card.
- Ready: move the flag to
ready; orgs that did not opt in now get the card too. - GA: when the flag is deleted, move its entry to
general_availablein the same PR if it should still be announced (dropannounceandwarningOverride), or just delete it. - No setting at all: add an entry to
general_available. - Stop announcing: set
announce: false/ remove thegeneral_availableentry, 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.
Media and Help Center link (R2) #
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"—srcis 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 overfallback(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.mp4and pointsrcat it.assets.jsonis cached for 5 minutes (plus 5 minutes in the webapp’s route cache).