--- id: "ADR-005" title: "Sistema de Feature Flags" type: "ADR" status: "Accepted" priority: "P0" supersedes: "N/A" superseded_by: "N/A" version: "1.0.0" created_date: "2026-01-07" updated_date: "2026-01-10" --- # ADR-005: Sistema de Feature Flags ## Metadata | Campo | Valor | |-------|-------| | ID | ADR-005 | | Estado | Accepted | | Fecha | 2026-01-10 | | Supersede | N/A | ## Contexto Template SaaS necesita control granular sobre features para: - Habilitar/deshabilitar funcionalidades por plan - Rollout gradual de nuevas features - A/B testing - Kill switches para emergencias - Features beta para usuarios seleccionados ## Opciones Consideradas ### Opción 1: Feature Flags en Código (Constantes) **Descripción:** Flags hardcodeados en el código. **Pros:** - Extremadamente simple - Sin dependencias **Contras:** - Requiere deploy para cambiar - Sin granularidad por tenant/user - Sin rollout gradual ### Opción 2: Servicio Externo (LaunchDarkly, Split) **Descripción:** Plataforma SaaS de feature flags. **Pros:** - Features avanzadas - UI de administración - Analytics incluido **Contras:** - Costo adicional - Dependencia externa - Latencia de red ### Opción 3: Sistema Propio con DB ✓ **Descripción:** Feature flags almacenados en PostgreSQL. **Pros:** - Control total - Sin costos adicionales - Personalizable - Datos en nuestra DB - Sin latencia externa **Contras:** - Desarrollo inicial - UI propia necesaria - Mantenimiento ## Decisión **Elegimos sistema propio** porque: 1. **Integración:** Se integra naturalmente con RLS y planes 2. **Costo:** Sin gastos adicionales 3. **Control:** Personalización según necesidades 4. **Simplicidad:** Casos de uso bien definidos ## Implementación ### Modelo de Datos ```sql -- Definición de flags CREATE TABLE feature_flags.flags ( id UUID PRIMARY KEY, key VARCHAR(100) UNIQUE NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, status flag_status DEFAULT 'disabled', default_value BOOLEAN DEFAULT false, rollout_percentage INTEGER DEFAULT 0, rollout_stage rollout_stage DEFAULT 'development', metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW() ); -- Override por tenant CREATE TABLE feature_flags.tenant_flags ( id UUID PRIMARY KEY, tenant_id UUID REFERENCES tenants.tenants(id), flag_id UUID REFERENCES feature_flags.flags(id), enabled BOOLEAN NOT NULL, UNIQUE(tenant_id, flag_id) ); -- Override por usuario CREATE TABLE feature_flags.user_flags ( id UUID PRIMARY KEY, user_id UUID REFERENCES users.users(id), flag_id UUID REFERENCES feature_flags.flags(id), enabled BOOLEAN NOT NULL, UNIQUE(user_id, flag_id) ); ``` ### Estados de Flag ```typescript enum FlagStatus { DISABLED = 'disabled', // Off para todos ENABLED = 'enabled', // On para todos PERCENTAGE = 'percentage', // Rollout gradual USER_LIST = 'user_list', // Solo usuarios específicos } enum RolloutStage { DEVELOPMENT = 'development', // Solo devs INTERNAL = 'internal', // Equipo interno BETA = 'beta', // Beta testers GENERAL = 'general', // Todos } ``` ### Lógica de Evaluación ```typescript async evaluateFlag( flagKey: string, context: { tenantId: string; userId: string } ): Promise { const flag = await this.getFlag(flagKey); if (!flag) return false; // 1. Check user override const userOverride = await this.getUserOverride(flag.id, context.userId); if (userOverride !== null) return userOverride; // 2. Check tenant override const tenantOverride = await this.getTenantOverride(flag.id, context.tenantId); if (tenantOverride !== null) return tenantOverride; // 3. Check flag status switch (flag.status) { case 'disabled': return false; case 'enabled': return true; case 'percentage': return this.isInRolloutPercentage(context.userId, flag.rollout_percentage); case 'user_list': return this.isInUserList(flag.id, context.userId); } return flag.default_value; } // Deterministic rollout based on user ID isInRolloutPercentage(userId: string, percentage: number): boolean { const hash = this.hashUserId(userId); return (hash % 100) < percentage; } ``` ### Integración con Plans ```typescript // Feature habilitada por plan async isPlanFeatureEnabled( tenantId: string, featureKey: string ): Promise { const subscription = await this.getSubscription(tenantId); const plan = await this.getPlan(subscription.plan_id); return plan.features[featureKey] === true; } // Combinar plan + flag async isFeatureEnabled( featureKey: string, context: { tenantId: string; userId: string } ): Promise { // Primero verificar plan const planEnabled = await this.isPlanFeatureEnabled(context.tenantId, featureKey); if (!planEnabled) return false; // Luego verificar flag return this.evaluateFlag(featureKey, context); } ``` ### Frontend Hook ```typescript function useFeatureFlag(key: string): boolean { const { user, tenant } = useAuth(); const { data: flags } = useQuery(['flags'], fetchFlags); return useMemo(() => { return evaluateClientFlag(key, flags, { userId: user.id, tenantId: tenant.id }); }, [key, flags, user.id, tenant.id]); } // Uso function MyComponent() { const showNewFeature = useFeatureFlag('new_dashboard'); if (!showNewFeature) return ; return ; } ``` ### API Endpoints | Método | Endpoint | Descripción | |--------|----------|-------------| | GET | `/flags` | Listar flags evaluados | | GET | `/flags/:key` | Evaluar flag específico | | POST | `/admin/flags` | Crear flag (admin) | | PATCH | `/admin/flags/:id` | Actualizar flag (admin) | | POST | `/admin/flags/:id/tenant-override` | Override por tenant | ## Consecuencias ### Positivas - Control total sobre features - Sin dependencias externas - Integración natural con planes - Rollout gradual soportado - Caching eficiente posible ### Negativas - UI de administración necesaria - Sin analytics avanzado built-in - Mantenimiento propio ### Caching Strategy - Cache en memoria por request - Invalidación en cambio de flag - TTL de 5 minutos para flags globales ## Referencias - [Feature Flags Best Practices](https://martinfowler.com/articles/feature-toggles.html) - [Rollout Strategies](https://launchdarkly.com/blog/feature-flag-rollout/) - Implementación: `apps/backend/src/modules/feature-flags/` --- **Fecha decision:** 2026-01-10 **Autores:** Claude Code (Arquitectura)