Feature Flag System Migration: Legacy to Domain-Driven Architecture
SUPERSEDED — historical only. This document planned a migration onto Flipper, which #1409 has since removed entirely. The
domains/system is now backed bysystem.feature_flags+system.feature_flag_overrideswith no gem involved, and anyFlipper.*call below will raiseNameError.For how feature flags actually work, read Feature Flags.
The legacy
parts/analysis below is still accurate and is why the doc is kept.
Original status: Documentation Only - No implementation yet
This document outlines the migration strategy from the legacy feature flag system (in backend/parts/) to the new domain architecture (in backend/domains/), supporting three entity types (User, Tenant, Practitioner).
1. Legacy Feature Flag System Summary
1.1 Current Endpoints
| Endpoint | Controller | Entity | Operations |
|---|---|---|---|
GET /users/:user_id/user_flags | UserFeatureFlagsController | User | read, update |
PUT /users/:user_id/user_flags/:flag_id | UserFeatureFlagsController | User | update |
GET /clinics/:clinic_id/clinic_flags | ClinicFeatureFlagsController | Clinic | read, update |
PUT /clinics/:clinic_id/clinic_flags/:id | ClinicFeatureFlagsController | Clinic | update |
GET /clinic_doctors/:clinic_doctor_id/feature_flags | ClinicDoctorFeatureFlagsController | ClinicDoctor | read, update |
PUT /clinic_doctors/:clinic_doctor_id/feature_flags/:flag_id | ClinicDoctorFeatureFlagsController | ClinicDoctor | update |
1.2 Legacy Database Schema
Table: public.basic_feature_flags (definition store)
| Column | Type | Description |
|---|---|---|
| id | bigint | Primary key |
| flag_name | varchar | Unique flag identifier (e.g., "ENABLE_SURGERY") |
| entity_type | varchar | "User", "Clinic", "ClinicDoctor" |
| default_enabled | boolean | Default state |
| category | varchar | Grouping (e.g., "ui_features") |
| description | text | Human-readable description |
Table: public.feature_flags (overrides)
| Column | Type | Description |
|---|---|---|
| id | bigint | Primary key |
| basic_feature_flag_id | bigint | FK to basic_feature_flags |
| entity_id | bigint | user_id, clinic_id, or clinic_doctor_id |
| entity_type | varchar | Entity type |
| enabled | boolean | Override state |
| metadata | jsonb | Additional data |
1.3 Available Legacy Flags (19 total)
App Visibility (5)
SHOW_CMS_APP,SHOW_GROWTH_APP,SHOW_REPORTS_APP,SHOW_CONNECT_APP,SHOW_HOSPITAL_APP
Core Features (2)
ENABLE_PACKAGES,ENABLE_GROWTH
Analytics & Reporting (2)
ENABLE_REV_SHARE,ENABLE_COMMISSION_TRACKING
Experimental (1)
ENABLE_BETA_FEATURES
Search (1)
ENABLE_ADVANCED_SEARCH
Clinical Features (4)
ENABLE_SURGERY,ENABLE_IPD,ENABLE_ABDM,ENABLE_WORKFLOWS
AI Features (1)
ENABLE_AI_ASSISTANT
Scope note (#2505). #2505 retired these keys from the native
system.feature_flagscatalog only, where the domains encounter workflow now follows the practitioner's module entitlement instead. The legacy rows and constants documented in this section are untouched and still gateparts/code (for example the dental examination API readsENABLE_DENTAL_MODULEhere).
Specialist Modules (3)
ENABLE_PSYCHIATRIST_MODULE,ENABLE_PEDIATRICIAN_MODULE,ENABLE_DERMATOLOGIST_MODULE
1.4 Legacy Business Rules
-
One Specialty Per Doctor: When enabling a specialist module (e.g.,
ENABLE_PEDIATRICIAN_MODULE), other specialist modules are automatically disabled for that doctor. -
Authorization:
- User flags: Requires authentication (via ApiController)
- Clinic flags: Requires
user.authorized_clinic?(clinic_id) - Doctor flags: Requires
user.authorized_clinic?(clinic_id)
2. New Domain Architecture
2.1 Architecture Overview
The new architecture uses:
- Flipper gem with PostgreSQL storage
- Domain-based schema (
system.,reference.) - UUID primary keys
- Blueprinter for serialization
- Concern-based integration (
FeatureFlaggable)
2.2 New Database Schema
Table: system.flipper_features (feature definitions)
| Column | Type | Description |
|---|---|---|
| id | uuid | Primary key |
| key | varchar(255) | Feature name |
| created_at | timestamptz | Creation timestamp |
| updated_at | timestamptz | Update timestamp |
Table: reference.flipper_gates (feature rules)
| Column | Type | Description |
|---|---|---|
| id | uuid | Primary key |
| feature_key | varchar(255) | Reference to feature |
| key | varchar(255) | Gate type: "boolean", "entity_type", "actors", "percentage_of_time" |
| value | text | Gate value |
| created_at | timestamptz | Creation timestamp |
| updated_at | timestamptz | Update timestamp |
2.3 Entity Type Mapping
| Legacy Entity | New Domain Entity | Entity Type |
|---|---|---|
| User | Identity::User | User |
| Clinic | Identity::Tenant | Tenant |
| ClinicDoctor | Identity::Practitioner | Practitioner |
2.4 New API Endpoints (Admin Only)
| Endpoint | Description |
|---|---|
GET /system/feature_flags | List all feature flags |
GET /system/feature_flags/:key | Get flag details |
PUT /system/feature_flags/enable_actor | Enable for actor |
DELETE /system/feature_flags/disable_actor | Disable for actor |
2.5 New Integration Pattern
# Include in any model
class User < Identity::BaseRecord
include Identity::Concerns::FeatureFlaggable
end
# Usage
user = User.find(id)
user.has_feature?('SHOW_CMS_APP') # Check if enabled
user.enable_feature!('SHOW_CMS_APP') # Enable (admin)
user.disable_feature!('SHOW_CMS_APP') # Disable (admin)3. Gap Analysis
3.1 Comparison Matrix
| Capability | Legacy System | New System | Gap |
|---|---|---|---|
| Entity Types | User, Clinic, ClinicDoctor | User, Tenant, Practitioner | Partial (mapping exists) |
| Read Flags | ✅ Yes | ✅ Yes (via concern) | N/A |
| Update Flags | ✅ Yes | ✅ Yes (admin API) | N/A |
| Default Values | ✅ Yes (basic_feature_flags) | ✅ Yes (boolean gate) | N/A |
| Categories | ✅ Yes | ❌ No | Add category field |
| Metadata | ✅ Yes (jsonb) | ✅ Yes (in gates) | N/A |
| Specialist Rules | ✅ Yes (one per doctor) | ❌ No | Implement in logic |
| Public API | ✅ Yes | ❌ Admin only | Add public endpoints |
| Authorization | Per-endpoint | Admin only | Add role-based access |
3.2 Missing Components
- Public API for non-admin users - Need endpoints for users to read their flags
- Category support - Flags should be categorizable (UI, Clinical, etc.)
- Specialist module business logic - One specialty per doctor rule needs implementation
- Legacy entity mapping - Need seamless mapping between Clinic→Tenant and ClinicDoctor→Practitioner
4. Proposed Domain Design
4.1 Domain Structure
Create a new feature_flags domain or expand the existing system domain:
backend/domains/feature_flags/
├── models/
│ ├── feature_flag.rb # Feature definition
│ ├── flag_assignment.rb # Entity-to-flag mappings
│ └── concerns/
│ └── feature_flaggable.rb # Already exists in Identity
├── controllers/
│ ├── feature_flags_controller.rb # Admin management
│ ├── user_flags_controller.rb # User-level flags
│ ├── tenant_flags_controller.rb # Clinic-level flags
│ └── practitioner_flags_controller.rb # Doctor-level flags
├── services/
│ └── feature_flag_service.rb # Shared business logic
├── interactor/
│ ├── list_flags.rb
│ ├── update_flag.rb
│ └── validate_specialist_rule.rb
├── blueprints/
│ ├── feature_flag_blueprint.rb
│ └── flag_assignment_blueprint.rb
└── routes.rb4.2 Recommended Approach: Flipper-First
Leverage Flipper's existing capabilities:
- Feature definitions →
system.flipper_features - Entity restrictions →
reference.flipper_gates(entity_type gate) - Entity assignments →
reference.flipper_gates(actors gate) - Categories → Add
categoryfield to FlipperFeature model - Metadata → Use Flipper's built-in storage or extend
5. API Design
5.1 New Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /users/:id/feature_flags | Get user's flags | User or Admin |
| PUT | /users/:id/feature_flags/:key | Update user's flag | Admin |
| GET | /tenants/:id/feature_flags | Get tenant's flags | Tenant auth |
| PUT | /tenants/:id/feature_flags/:key | Update tenant's flag | Admin |
| GET | /practitioners/:id/feature_flags | Get practitioner's flags | Practitioner or Admin |
| PUT | /practitioners/:id/feature_flags/:key | Update practitioner's flag | Admin |
5.2 Request/Response Formats
GET /users/:id/feature_flags
// Request
GET /users/019bd563-5763-7895-b971-07436bd3f2cc/feature_flags
// Response (200 OK)
{
"data": {
"entity_type": "User",
"entity_id": "019bd563-5763-7895-b971-07436bd3f2cc",
"flags": [
{
"key": "SHOW_CMS_APP",
"enabled": true,
"category": "app_visibility",
"description": "Access to Clinical Management System",
"is_default": false
},
{
"key": "ENABLE_SURGERY",
"enabled": false,
"category": "clinical_modules",
"description": "Enable surgery module",
"is_default": false
}
]
}
}PUT /users/:id/feature_flags/:key
// Request
PUT /users/019bd563-5763-7895-b971-07436bd3f2cc/feature_flags/SHOW_CMS_APP
Content-Type: application/json
{
"enabled": true
}
// Response (200 OK)
{
"success": true,
"data": {
"key": "SHOW_CMS_APP",
"enabled": true,
"entity_type": "User",
"entity_id": "019bd563-5763-7895-b971-07436bd3f2cc"
}
}5.3 Authorization Rules
| Endpoint | User | Admin | Clinic Owner |
|---|---|---|---|
| GET /users/:id/feature_flags | ✅ (own) | ✅ | ❌ |
| PUT /users/:id/feature_flags/:key | ❌ | ✅ | ❌ |
| GET /tenants/:id/feature_flags | ❌ | ✅ | ✅ (own) |
| PUT /tenants/:id/feature_flags/:key | ❌ | ✅ | ❌ |
| GET /practitioners/:id/feature_flags | ✅ (own) | ✅ | ✅ (own clinic) |
| PUT /practitioners/:id/feature_flags/:key | ❌ | ✅ | ✅ (own clinic) |
6. Migration Strategy
6.1 Backend Changes
Phase 1: Schema Preparation
- Add
categoryfield tosystem.flipper_features - Add seed data for existing 19 flags with proper entity types
- Create migration script to copy legacy flag definitions
Phase 2: Controller Implementation
- Create new domain controllers:
UserFeatureFlagsController(delegates to new architecture)TenantFeatureFlagsController(for clinics)PractitionerFeatureFlagsController(for doctors)
- Update routes to point to new controllers
- Implement authorization policies
Phase 3: Business Logic
- Implement
FeatureFlagServicewith shared logic - Add specialist module rule (one per practitioner)
- Add default value handling
Phase 4: Deprecation
- Keep legacy controllers as thin wrappers
- Add deprecation warnings to legacy endpoints
- Log migration progress
6.2 Data Migration
Step 1: Map Legacy Flags to New Schema
LEGACY_FLAG_MAPPING = {
# User-level flags
'SHOW_CMS_APP' => { entity_type: 'User', category: 'app_visibility' },
'SHOW_GROWTH_APP' => { entity_type: 'User', category: 'app_visibility' },
'SHOW_REPORTS_APP' => { entity_type: 'User', category: 'app_visibility' },
'SHOW_CONNECT_APP' => { entity_type: 'User', category: 'app_visibility' },
'SHOW_HOSPITAL_APP' => { entity_type: 'User', category: 'app_visibility' },
'ENABLE_BETA_FEATURES' => { entity_type: 'User', category: 'experimental' },
# Tenant (Clinic)-level flags
'ENABLE_PACKAGES' => { entity_type: 'Tenant', category: 'core_features' },
'ENABLE_GROWTH' => { entity_type: 'Tenant', category: 'core_features' },
'ENABLE_REV_SHARE' => { entity_type: 'Tenant', category: 'analytics' },
'ENABLE_COMMISSION_TRACKING' => { entity_type: 'Tenant', category: 'analytics' },
'ENABLE_SURGERY' => { entity_type: 'Tenant', category: 'clinical_modules' },
'ENABLE_IPD' => { entity_type: 'Tenant', category: 'clinical_modules' },
'ENABLE_ABDM' => { entity_type: 'Tenant', category: 'clinical_modules' },
'ENABLE_WORKFLOWS' => { entity_type: 'Tenant', category: 'clinical_modules' },
'ENABLE_ADVANCED_SEARCH' => { entity_type: 'Tenant', category: 'search' },
'ENABLE_AI_ASSISTANT' => { entity_type: 'Tenant', category: 'ai' },
# Practitioner (ClinicDoctor)-level flags
'ENABLE_PSYCHIATRIST_MODULE' => { entity_type: 'Practitioner', category: 'specialist_modules' },
'ENABLE_PEDIATRICIAN_MODULE' => { entity_type: 'Practitioner', category: 'specialist_modules' },
'ENABLE_DERMATOLOGIST_MODULE' => { entity_type: 'Practitioner', category: 'specialist_modules' }
}6.3 Frontend Updates
API Service Updates
Update /frontend/packages/auth/src/services/featureFlagsApi.ts:
const API = {
// User flags
getUserFlags: (userId: string) =>
api.get(`/users/${userId}/feature_flags`),
updateUserFlag: (userId: string, flagKey: string, enabled: boolean) =>
api.put(`/users/${userId}/feature_flags/${flagKey}`, { enabled }),
// Tenant (Clinic) flags
getClinicFlags: (clinicId: string) =>
api.get(`/tenants/${clinicId}/feature_flags`),
updateClinicFlag: (clinicId: string, flagKey: string, enabled: boolean) =>
api.put(`/tenants/${clinicId}/feature_flags/${flagKey}`, { enabled }),
// Practitioner flags
getDoctorFlags: (doctorId: string) =>
api.get(`/practitioners/${doctorId}/feature_flags`),
updateDoctorFlag: (doctorId: string, flagKey: string, enabled: boolean) =>
api.put(`/practitioners/${doctorId}/feature_flags/${flagKey}`, { enabled })
}7. Implementation Timeline
| Phase | Task | Effort |
|---|---|---|
| 1 | Schema preparation & seed data | 1 day |
| 2 | Controller implementation | 2 days |
| 3 | Business logic & policies | 2 days |
| 4 | Data migration script | 1 day |
| 5 | Frontend API updates | 1 day |
| 6 | Testing & validation | 2 days |
| 7 | Parallel running (legacy + new) | 1 week |
| 8 | Cutover & deprecation | 1 day |
Total Estimated: ~11 days
8. Risks & Mitigation
| Risk | Impact | Mitigation |
|---|---|---|
| Entity ID Mapping | Legacy uses integer IDs, new uses UUIDs | Create LegacyIdMapper service |
| Specialist Module Rule | One specialty per doctor must be enforced | Implement in controller update logic |
| Authorization Gaps | Different auth between legacy and new | Define clear policy matrix |
| Downtime | Migration could cause brief downtime | Zero-downtime migration, keep legacy as fallback |
9. Key Files Reference
Backend - New Architecture (Modify)
| File | Purpose |
|---|---|
backend/domains/system/controllers/feature_flags_controller.rb | Admin management |
backend/domains/system/models/flipper_feature.rb | Add category field |
backend/config/routes.rb | Add new routes |
Backend - Legacy (Deprecate)
| File | Purpose |
|---|---|
backend/parts/core/controllers/user_feature_flags_controller.rb | Deprecate |
backend/parts/clinic/controllers/clinic_feature_flags_controller.rb | Deprecate |
backend/parts/clinic/controllers/clinic_doctor_feature_flags_controller.rb | Deprecate |
Frontend - Modify
| File | Purpose |
|---|---|
frontend/packages/auth/src/services/featureFlagsApi.ts | Update endpoints |
frontend/packages/auth/src/constants/featureFlags.ts | Update flag list |
10. Conclusion
The migration from the legacy feature flag system to the new Flipper-based architecture is feasible and recommended because:
- The new system already provides a solid foundation
- Flipper is a well-maintained, production-ready gem
- The entity mapping (User→User, Clinic→Tenant, ClinicDoctor→Practitioner) is already defined
- The frontend can continue using existing hooks/components with minimal changes
Key work items:
- Add missing capabilities (categories, public API)
- Implement the new controllers and routes
- Create the data migration script
- Update frontend API calls