template-saas/docs/architecture/adr/ADR-005-feature-flags.md
rckrdmrd 50a821a415
Some checks failed
CI / Backend CI (push) Has been cancelled
CI / Frontend CI (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / CI Summary (push) Has been cancelled
[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Actualizaciones de configuracion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 08:53:08 -06:00

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:

  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

-- 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


Fecha decision: 2026-01-10 Autores: Claude Code (Arquitectura)