template-saas/docs/97-adr/adr/ADR-005-feature-flags.md
Adrian Flores Cortes 806612a4db
Some checks are pending
CI / Backend CI (push) Waiting to run
CI / Frontend CI (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / CI Summary (push) Blocked by required conditions
[REESTRUCTURA-DOCS] refactor: Corregir estructura docs/ segun SIMCO-DOCUMENTACION-PROYECTO
- Renombrar 02-integraciones/ → 03-integraciones/ (resolver prefijo duplicado)
- Renombrar 02-devops/ → 04-devops/ (resolver prefijo duplicado)
- Renombrar architecture/ → 97-adr/ (agregar prefijo numerico)
- Actualizar _MAP.md con nueva estructura y version 2.1.0

Estructura final:
- 00-vision-general/
- 01-modulos/
- 02-especificaciones/
- 03-integraciones/
- 04-devops/
- 97-adr/

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 20:34:14 -06:00

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)