Feature Flags
Open Chat Studio uses Django Waffle for feature flags with a custom team-based implementation. Feature flags allow you to toggle features on/off for specific teams without code deployments.
Overview
Feature flags in Open Chat Studio are:
- Team-scoped: Flags can be enabled/disabled per team
- Database-driven: Stored in the database and configurable via a custom admin page
- Cached: Uses Redis for performance
- Convention-based: New flags must follow naming conventions and be registered in apps/teams/flags.py
Custom Flag Model
The system uses a custom Flag model (apps.teams.models.Flag) that extends Django Waffle's AbstractUserFlag to support team-based activation:
from apps.teams.models import Flag
# Create a flag
flag = Flag.objects.create(name="flag_new_feature")
# Activate for specific teams
flag.teams.add(my_team)
# Check if active for a team
flag.is_active_for_team(my_team)
Tip
Flags are created automatically in the database when referenced in code or templates so it is not necessary to create them manually.
Registering a Flag
All flags must be declared in apps/teams/flags.py as entries in the Flags enum before use:
from apps.teams.flags import Flags
class Flags(FlagInfo, Enum):
MY_FEATURE = (
"flag_my_feature", # slug — must start with "flag_"
"Short description", # shown in admin and team settings UI
"docs-slug", # key into settings.DOCUMENTATION_LINKS (use "" if none)
[], # other flag slugs this flag requires (auto-enabled together)
True, # teams_can_manage: whether team admins can toggle this themselves
)
This registry drives the team settings UI (teams_can_manage=True flags appear there) and the check_flag_usage audit command. Flags are created in the database automatically on first use — no migration needed.
When referencing a flag in Python code, prefer the enum to avoid string typos:
from apps.teams.flags import Flags
from waffle import flag_is_active
if flag_is_active(request, Flags.MY_FEATURE.slug):
...
Naming Convention
All new feature flags MUST be prefixed with flag_
# ✅ Correct
"flag_new_dashboard"
"flag_enhanced_chat"
# ❌ Incorrect - will raise ValidationError
"new_dashboard"
This naming convention:
- Prevents naming conflicts
- Makes flags easily identifiable in code
- Enables better tooling and management
Usage in Code
Python
In Django views or other Python code where a request object is available, you can check if a feature flag is active for the current team or user:
from waffle import flag_is_active
def my_view(request):
if flag_is_active(request, "flag_new_feature"):
# New feature code
return render(request, "new_template.html")
else:
# Legacy code
return render(request, "old_template.html")
When a request object is not available, you can still check if a flag is active by using the Flag model directly:
flag = Flag.get("flag_new_feature")
flag.is_active_for_team(team)
flag.is_active_for_user(user)
Django Templates
{% load waffle_tags %}
{% flag "flag_new_feature" %}
<div class="new-feature">
<!-- New feature UI -->
</div>
{% endflag %}
Pipeline node fields
For pipeline node fields that should only be visible in the UI when a flag is active, use flag_required:
my_field: str = Field(..., flag_required="flag_my_feature")
Removing a Flag (Rolling Out to Everyone)
When a feature is stable and should be on for all users, remove the flag entirely rather than leaving dead code:
- Run
check_flag_usageto find every reference. - Remove all
flag_is_active/{% flag %}/override_flagguards, keeping the guarded code. - Delete the
Flagsenum entry fromapps/teams/flags.py. - Update or remove tests that used
override_flagfor this flag — test the behaviour unconditionally. - The flag row in the database becomes orphaned and can be deleted via the Django admin.
Management Commands
Check Flag Usage
Use the check_flag_usage management command to find where flags are used in the codebase:
# Check all flags
python manage.py check_flag_usage
# Check specific flag
python manage.py check_flag_usage --flag-name flag_new_feature
Output example:
Found 5 flags in database
Flags found in code (3):
✓ flag_new_dashboard
- apps/web/views.py
- templates/dashboard.html
✓ flag_enhanced_chat
- apps/chat/views.py
Flags not found in code (2):
✗ flag_old_feature
✗ flag_experimental_ui
This helps identify:
- Active flags: Currently used in code
- Dead flags: No longer referenced and can be removed
Best Practices
Naming
- Use descriptive names:
flag_enhanced_searchnotflag_search - Include the feature area:
flag_chat_reactions,flag_dashboard_v2 - Avoid abbreviations:
flag_new_authenticationnotflag_new_auth
Code Organization
- Keep flag logic simple and readable
- Avoid deep nesting of feature flag conditions
- Consider extracting flag-dependent code into separate functions/classes
# ✅ Good
def get_dashboard_data(request):
if flag_is_active(request, 'flag_new_dashboard'):
return get_enhanced_dashboard_data(request)
return get_legacy_dashboard_data(request)
# ❌ Avoid deep nesting
def complex_view(request):
if flag_is_active(request, 'flag_feature_a'):
if flag_is_active(request, 'flag_feature_b'):
# Deep nesting makes code hard to follow
Documentation
- Document the purpose of each flag
- Include rollout plans in PR descriptions
- Update team documentation when adding user-facing features
Testing
- Test both flag states (enabled/disabled)
- Include flag state in test names
def test_dashboard_with_new_feature_enabled(self):
with override_flag('flag_new_dashboard', active=True):
# Test new behavior
def test_dashboard_with_new_feature_disabled(self):
with override_flag('flag_new_dashboard', active=False):
# Test legacy behavior