- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones de configuracion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
6.4 KiB
6.4 KiB
| id | title | type | status | priority | supersedes | superseded_by | version | created_date | updated_date |
|---|---|---|---|---|---|---|---|---|---|
| ADR-005 | Sistema de Feature Flags | ADR | Accepted | P0 | N/A | N/A | 1.0.0 | 2026-01-07 | 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:
- Integración: Se integra naturalmente con RLS y planes
- Costo: Sin gastos adicionales
- Control: Personalización según necesidades
- Simplicidad: Casos de uso bien definidos
Implementación
Modelo de Datos
-- 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
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
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
// 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
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
- Rollout Strategies
- Implementación:
apps/backend/src/modules/feature-flags/
Fecha decision: 2026-01-10 Autores: Claude Code (Arquitectura)