Construye un Sistema de Feature Flags en 50 Líneas con Rails.app.creds
No necesitas LaunchDarkly. No necesitas Flipper. Para la mayoría de las aplicaciones Rails, necesitas feature flags que estén controlados por versiones, sean fáciles de sobrescribir en desarrollo, y no requieran otro servicio para administrar.
Rails.app.creds de Rails 8.2 te da exactamente eso. Aquí tienes un sistema completo de feature flags en menos de 50 líneas de código.
La Idea Central
Rails.app.creds primero verifica ENV, luego las credenciales encriptadas. Esto significa:
- Almacena tus flags en
credentials.yml.enc(controlado por versiones, revisado en PRs) - Sobrescribe con variables ENV en cualquier momento (testing, debugging, rollouts graduales)
Sin base de datos. Sin servicio externo. Sin llamadas API.
Paso 1: Almacena Flags en Credentials
bin/rails credentials:edit
# config/credentials.yml.enc
features:
new_checkout: false
dark_mode: false
ai_summaries: false
beta_dashboard: false
Estos son tus valores por defecto. Están encriptados, controlados por versiones, y requieren un PR para cambiar.
Paso 2: Crea el Módulo de Feature Flags
# app/models/concerns/feature_flags.rb
module FeatureFlags
extend self
def enabled?(flag_name)
value = Rails.app.creds.option(:features, flag_name, default: false)
ActiveModel::Type::Boolean.new.cast(value)
end
def disabled?(flag_name)
!enabled?(flag_name)
end
def enable!(flag_name)
Current.feature_overrides ||= {}
Current.feature_overrides[flag_name] = true
end
def disable!(flag_name)
Current.feature_overrides ||= {}
Current.feature_overrides[flag_name] = false
end
def with(flag_name, value)
old_value = Current.feature_overrides&.dig(flag_name)
enable!(flag_name) if value
disable!(flag_name) unless value
yield
ensure
if old_value.nil?
Current.feature_overrides&.delete(flag_name)
else
Current.feature_overrides[flag_name] = old_value
end
end
end
Paso 3: Añade Sobrescrituras con Alcance por Request
Para control de flags por request (testing, sobrescrituras de admin), conecta Current:
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :feature_overrides
end
# lib/feature_override_configuration.rb
class FeatureOverrideConfiguration
def require(*keys)
option(*keys)
end
def option(*keys, default: nil)
return default unless keys.first == :features && keys.length == 2
Current.feature_overrides&.dig(keys.last)
end
def keys = []
def reload = nil
end
Actualiza la cadena de credenciales:
# config/initializers/credentials.rb
Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
FeatureOverrideConfiguration.new, # Alcance por request primero
Rails.app.envs, # Luego ENV
Rails.app.credentials # Luego archivo encriptado
)
Paso 4: Úsalo en Todas Partes
En controladores
class CheckoutController < ApplicationController
def show
if FeatureFlags.enabled?(:new_checkout)
render :new_checkout
else
render :legacy_checkout
end
end
end
En vistas
<% if FeatureFlags.enabled?(:dark_mode) %>
<body class="dark">
<% else %>
<body>
<% end %>
En modelos
class Order < ApplicationRecord
def calculate_shipping
if FeatureFlags.enabled?(:new_shipping_calculator)
NewShippingCalculator.calculate(self)
else
legacy_shipping_calculation
end
end
end
En tests
class CheckoutTest < ActionDispatch::IntegrationTest
test "nuevo flujo de checkout" do
FeatureFlags.with(:new_checkout, true) do
get checkout_path
assert_select ".new-checkout-form"
end
end
test "flujo de checkout legacy" do
FeatureFlags.with(:new_checkout, false) do
get checkout_path
assert_select ".legacy-checkout-form"
end
end
end
Sobrescribiendo Flags
En desarrollo
Sobrescribe cualquier flag sin editar credentials:
# Habilitar un flag
FEATURES__NEW_CHECKOUT=true bin/rails server
# Habilitar múltiples
FEATURES__NEW_CHECKOUT=true FEATURES__DARK_MODE=true bin/rails server
En staging/producción
Configura variables ENV en tu configuración de despliegue:
# fly.toml, render.yaml, etc.
[env]
FEATURES__BETA_DASHBOARD = "true"
Por request (herramientas de admin)
class Admin::FeatureFlagsController < AdminController
def toggle
flag = params[:flag].to_sym
if FeatureFlags.enabled?(flag)
FeatureFlags.disable!(flag)
else
FeatureFlags.enable!(flag)
end
redirect_back fallback_location: admin_root_path
end
end
Avanzado: Rollouts por Porcentaje
¿Quieres habilitar una funcionalidad para el 10% de los usuarios?
# app/models/concerns/feature_flags.rb
module FeatureFlags
def enabled_for?(flag_name, user)
# Verificar si está explícitamente habilitado/deshabilitado primero
return enabled?(flag_name) if explicitly_set?(flag_name)
# Verificar rollout por porcentaje
percentage = Rails.app.creds.option(:features, :"#{flag_name}_percentage", default: nil)
return enabled?(flag_name) unless percentage
# Bucketing consistente basado en user ID
bucket = Digest::MD5.hexdigest("#{flag_name}-#{user.id}").first(8).to_i(16) % 100
bucket < percentage.to_i
end
private
def explicitly_set?(flag_name)
Current.feature_overrides&.key?(flag_name) ||
ENV["FEATURES__#{flag_name.to_s.upcase}"].present?
end
end
Configura el rollout:
# credentials.yml.enc
features:
new_checkout: false
new_checkout_percentage: 10 # 10% de usuarios
Uso:
if FeatureFlags.enabled_for?(:new_checkout, current_user)
# 10% de usuarios ven esto
end
El mismo usuario siempre obtiene el mismo resultado (bucketing consistente vía hash MD5).
Avanzado: Segmentación por Usuario
Habilita funcionalidades para segmentos específicos de usuarios:
module FeatureFlags
def enabled_for?(flag_name, user)
return true if user_in_allowlist?(flag_name, user)
return true if segment_enabled?(flag_name, user)
return enabled_by_percentage?(flag_name, user)
end
private
def user_in_allowlist?(flag_name, user)
allowlist = Rails.app.creds.option(:features, :"#{flag_name}_users", default: [])
allowlist.include?(user.id) || allowlist.include?(user.email)
end
def segment_enabled?(flag_name, user)
segments = Rails.app.creds.option(:features, :"#{flag_name}_segments", default: [])
segments.any? { |segment| user_in_segment?(user, segment) }
end
def user_in_segment?(user, segment)
case segment.to_sym
when :staff then user.staff?
when :beta then user.beta_tester?
when :premium then user.premium?
else false
end
end
end
# credentials.yml.enc
features:
new_checkout: false
new_checkout_users:
- 123 # User ID
- "[email protected]" # O email
new_checkout_segments:
- staff
- beta
new_checkout_percentage: 5 # Más 5% de todos los demás
Estrategia de rollout: staff primero, luego beta testers, luego 5% de todos, luego aumentar el porcentaje a medida que crece la confianza.
La Implementación Completa
Aquí está todo en un solo lugar:
# app/models/concerns/feature_flags.rb (46 líneas)
module FeatureFlags
extend self
def enabled?(flag_name)
cast_boolean(Rails.app.creds.option(:features, flag_name, default: false))
end
def disabled?(flag_name) = !enabled?(flag_name)
def enabled_for?(flag_name, user)
return true if allowlisted?(flag_name, user)
return true if segment_match?(flag_name, user)
return percentage_match?(flag_name, user) if rollout_percentage(flag_name)
enabled?(flag_name)
end
def enable!(flag_name)
(Current.feature_overrides ||= {})[flag_name] = true
end
def disable!(flag_name)
(Current.feature_overrides ||= {})[flag_name] = false
end
def with(flag_name, value)
old = Current.feature_overrides&.dig(flag_name)
value ? enable!(flag_name) : disable!(flag_name)
yield
ensure
old.nil? ? Current.feature_overrides&.delete(flag_name) : Current.feature_overrides[flag_name] = old
end
private
def cast_boolean(value) = ActiveModel::Type::Boolean.new.cast(value)
def creds_option(flag, suffix) = Rails.app.creds.option(:features, :"#{flag}#{suffix}", default: nil)
def rollout_percentage(flag) = creds_option(flag, :_percentage)&.to_i
def allowlist(flag) = creds_option(flag, :_users) || []
def segments(flag) = creds_option(flag, :_segments) || []
def allowlisted?(flag, user)
list = allowlist(flag)
list.include?(user.id) || list.include?(user.email)
end
def segment_match?(flag, user)
segments(flag).any? { |s| user.try(:"#{s}?") }
end
def percentage_match?(flag, user)
Digest::MD5.hexdigest("#{flag}-#{user.id}").first(8).to_i(16) % 100 < rollout_percentage(flag)
end
end
Por Qué Esto Supera a una Solución SaaS
| Característica | Este enfoque | SaaS |
|---|---|---|
| Costo | Gratis | $20-500/mes |
| Latencia | Cero (en memoria) | Llamada API |
| Control de versiones | Sí (credentials) | Usualmente no |
| Code review | Sí (PRs) | Depende |
| Funciona offline | Sí | No |
| Complejidad | Baja | Media-Alta |
| Segmentación de usuarios | Básica | Avanzada |
| Analytics | DIY | Incorporado |
Para la mayoría de las apps, no necesitas actualizaciones de flags en tiempo real ni reglas de segmentación complejas. Necesitas flags que sean fáciles de administrar, no añadan latencia, y no cuesten dinero.
Cuándo Usar un Servicio de Feature Flags Real
Este enfoque tiene límites. Considera un servicio dedicado cuando necesites:
- Usuarios no técnicos administrando flags (product managers, etc.)
- Reglas de segmentación complejas (geografía, tipo de dispositivo, etc.)
- Actualizaciones en tiempo real sin deploys
- Experimentación incorporada y analytics de A/B testing
- Logs de auditoría y funcionalidades de cumplimiento
¿Pero para la mayoría de las apps Rails? 46 líneas de código y Rails.app.creds es todo lo que necesitas.
Conclusión
Rails.app.creds de Rails 8.2 con CombinedConfiguration es una base perfecta para feature flags:
- Almacena defaults en credenciales encriptadas (controlado por versiones)
- Sobrescribe con ENV (sin deploy necesario)
- Sobrescribe por request (testing, herramientas de admin)
- Añade rollouts por porcentaje y segmentación de usuarios según sea necesario
Sin dependencias externas. Sin latencia de API. Sin factura mensual.
Mira el PR #56404 para la implementación de Rails.app.creds.