- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones de configuracion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
269 lines
6.4 KiB
Markdown
269 lines
6.4 KiB
Markdown
---
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
// 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 <OldDashboard />;
|
|
return <NewDashboard />;
|
|
}
|
|
```
|
|
|
|
### 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)
|