5 Formas Inesperadas de Usar Rails.app.creds

Rails 8.2 introdujo Rails.app.creds como una forma unificada de acceder a credenciales desde ENV y archivos encriptados. Pero escondido en la implementación hay algo más poderoso: CombinedConfiguration es una cadena de responsabilidad para configuración. Cada backend devuelve un valor o lo pasa al siguiente.

Esto no es solo búsqueda de credenciales. Es middleware de configuración componible.

Aquí hay cinco patrones que aprovechan esta arquitectura.

1. Feature Flags Sin un Servicio

La mayoría de las apps no necesitan LaunchDarkly. Necesitas una forma de alternar funcionalidades que esté controlada por versiones y sea sobrescribible en desarrollo.

# config/credentials.yml.enc
features:
  new_checkout: false
  dark_mode: true
  ai_summaries: false

Accede a ellas como cualquier credencial:

if Rails.app.creds.option(:features, :new_checkout, default: false)
  render_new_checkout
else
  render_legacy_checkout
end

La magia: ENV sobrescribe las credenciales. Habilita una funcionalidad localmente sin tocar el archivo encriptado:

FEATURES__NEW_CHECKOUT=true bin/rails server

¿Necesitas probar una funcionalidad en staging? Configura la variable ENV en tu configuración de despliegue. Sin cambios de código, sin editar credenciales, sin deploy.

Haciéndolo ergonómico

Envuélvelo en un helper:

# app/helpers/feature_flags_helper.rb
module FeatureFlagsHelper
  def feature?(name)
    Rails.app.creds.option(:features, name, default: false).to_s == "true"
  end
end

# Uso
<% if feature?(:new_checkout) %>
  <%= render "checkout/new" %>
<% end %>

Ahora tienes feature flags que son:

  • Controlados por versiones (en credentials)
  • Sobrescribibles por entorno (vía ENV)
  • Sin dependencias externas
  • Gratis

2. Archivos de Sobrescritura para Desarrolladores

Todos los equipos tienen este problema: “¿Cómo pruebo con diferentes credenciales localmente sin commitearlas?”

Crea un archivo local de sobrescritura ignorado por git:

# config/initializers/developer_overrides.rb
class DeveloperOverrides
  def initialize
    path = Rails.root.join(".secrets.local.yml")
    @config = path.exist? ? YAML.load_file(path).deep_symbolize_keys : {}
  end

  def require(*keys)
    option(*keys) || raise(KeyError, "Faltante: #{keys.join('.')}")
  end

  def option(*keys, default: nil)
    value = keys.reduce(@config) { |h, k| h.is_a?(Hash) ? h[k] : nil }
    value.nil? ? default : value
  end

  def keys
    flatten_keys(@config)
  end

  def reload
    @config = YAML.load_file(Rails.root.join(".secrets.local.yml")).deep_symbolize_keys
  rescue Errno::ENOENT
    @config = {}
  end

  private

  def flatten_keys(hash, prefix = [])
    hash.flat_map do |k, v|
      v.is_a?(Hash) ? flatten_keys(v, prefix + [k]) : [prefix + [k]]
    end
  end
end

# Insertar con la más alta prioridad
if Rails.env.development?
  Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
    DeveloperOverrides.new,
    Rails.app.envs,
    Rails.app.credentials
  )
end

Agrega a .gitignore:

.secrets.local.yml

Ahora cualquier desarrollador puede crear su propio archivo de sobrescritura:

# .secrets.local.yml (ignorado por git)
stripe:
  secret_key: sk_test_mi_clave_de_prueba_personal
openai:
  api_key: sk-mi-propia-clave-openai
features:
  experimental_ui: true

No más incidentes de “accidentalmente comiteé mi API key”. No más “déjame editar temporalmente credentials.yml.enc”. Cada desarrollador tiene su propio sandbox.

3. Configuración con Alcance por Request

¿Qué tal si los admins pudieran sobrescribir la configuración por request? Útil para debugging, soporte al cliente o pruebas A/B.

# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :config_overrides
end

# lib/request_scoped_configuration.rb
class RequestScopedConfiguration
  def require(*keys)
    option(*keys) # Siempre retorna nil para continuar la cadena si no se encuentra
  end

  def option(*keys, default: nil)
    Current.config_overrides&.dig(*keys)
  end

  def keys
    Current.config_overrides&.keys || []
  end

  def reload
    # No-op, se limpia por request
  end
end

Conéctalo:

# config/initializers/credentials.rb
Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
  RequestScopedConfiguration.new,
  Rails.app.envs,
  Rails.app.credentials
)

Ahora en un controlador:

class Admin::DebugController < ApplicationController
  before_action :require_admin

  def impersonate_config
    # Usar temporalmente el modo de prueba de Stripe para esta sesión de admin
    Current.config_overrides = {
      stripe: { test_mode: true },
      features: { debug_panel: true }
    }
    redirect_back fallback_location: root_path, notice: "Modo debug habilitado"
  end
end

El resto de tu app no sabe ni le importa. Solo llama a Rails.app.creds.option(:stripe, :test_mode) y obtiene el valor correcto para el request actual.

4. Wrapper de Registro de Auditoría

Los requisitos de cumplimiento a menudo exigen registrar el acceso a credenciales sensibles. Con CombinedConfiguration, puedes envolver cualquier backend con logging:

# lib/audited_configuration.rb
class AuditedConfiguration
  def initialize(backend, logger: Rails.logger)
    @backend = backend
    @logger = logger
  end

  def require(*keys)
    log_access("require", keys)
    @backend.require(*keys)
  end

  def option(*keys, default: nil)
    log_access("option", keys)
    @backend.option(*keys, default: default)
  end

  def keys
    @backend.keys
  end

  def reload
    @backend.reload
  end

  private

  def log_access(method, keys)
    location = caller_locations(2, 1).first
    @logger.info({
      event: "credential_access",
      method: method,
      keys: keys.join("."),
      file: location.path,
      line: location.lineno,
      timestamp: Time.current.iso8601
    }.to_json)
  end
end

Envuelve tus credenciales de producción:

# config/initializers/credentials.rb
if Rails.env.production?
  audited_creds = AuditedConfiguration.new(Rails.app.credentials)

  Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
    Rails.app.envs,
    audited_creds
  )
end

Cada acceso a credenciales ahora se registra:

{"event":"credential_access","method":"require","keys":"stripe.secret_key","file":"app/services/payment_service.rb","line":42,"timestamp":"2026-01-05T10:30:00Z"}

Puedes alimentar esto a tu SIEM, configurar alertas para patrones de acceso inusuales, o satisfacer a los auditores que quieren prueba de controles de acceso.

5. Configuración Programada

¿Qué tal si la configuración pudiera cambiar basada en el tiempo? Precios de Black Friday. Temas festivos. Ventanas de mantenimiento programado.

# lib/scheduled_configuration.rb
class ScheduledConfiguration
  Schedule = Data.define(:range, :config)

  SCHEDULES = [
    Schedule.new(
      range: Time.zone.parse("2026-11-27")..Time.zone.parse("2026-12-01"),
      config: {
        pricing: { discount_percent: 30 },
        features: { sale_banner: true },
        cache: { ttl: 60 } # Cache más corto durante alto tráfico
      }
    ),
    Schedule.new(
      range: Time.zone.parse("2026-12-24")..Time.zone.parse("2026-12-26"),
      config: {
        features: { holiday_theme: true },
        support: { auto_reply: true }
      }
    )
  ]

  def require(*keys)
    option(*keys) || raise(KeyError, "Faltante: #{keys.join('.')}")
  end

  def option(*keys, default: nil)
    active = SCHEDULES.find { |s| s.range.cover?(Time.current) }
    return default unless active

    keys.reduce(active.config) { |h, k| h.is_a?(Hash) ? h[k] : nil }
  end

  def keys
    SCHEDULES.flat_map { |s| s.config.keys }.uniq
  end

  def reload
    # Podría recargar desde base de datos o fuente externa
  end
end

Conéctalo a la cadena:

Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
  ScheduledConfiguration.new,  # Sobrescrituras basadas en tiempo primero
  Rails.app.envs,
  Rails.app.credentials
)

En Black Friday, Rails.app.creds.option(:pricing, :discount_percent) automáticamente retorna 30. Sin necesidad de deploy. Cuando el horario termina, cae al valor por defecto.

Tu código de precios no necesita saber sobre Black Friday:

def apply_discount(price)
  discount = Rails.app.creds.option(:pricing, :discount_percent, default: 0)
  price * (1 - discount / 100.0)
end

El Patrón

Los cinco ejemplos comparten la misma perspectiva: CombinedConfiguration es middleware para configuración. El patrón de cadena de responsabilidad te permite:

  1. Agregar comportamientos en capas sin modificar código existente
  2. Componer funcionalidades ordenando backends
  3. Separar responsabilidades entre almacenamiento, control de acceso y lógica de negocio

El orden importa:

ActiveSupport::CombinedConfiguration.new(
  RequestScopedConfiguration.new,  # Mayor prioridad: sobrescrituras por request
  ScheduledConfiguration.new,       # Reglas basadas en tiempo
  DeveloperOverrides.new,           # Desarrollo local
  Rails.app.envs,                   # Variables de entorno
  AuditedConfiguration.new(         # Envuelto para logging
    Rails.app.credentials
  )
)

El primer valor no-nil gana. Cada backend puede manejar la solicitud o pasarla hacia abajo en la cadena.

Conclusión

Rails.app.creds parece una API de conveniencia simple. Pero CombinedConfiguration es un bloque de construcción para sistemas de configuración sofisticados:

  • Feature flags sin servicios externos
  • Sandboxes de desarrollador sin conflictos de credenciales
  • Debugging con alcance por request
  • Registro de auditoría listo para cumplimiento
  • Configuración basada en tiempo sin deploys

¿La mejor parte? Tu código de aplicación permanece limpio. Solo llama a Rails.app.creds.option(:whatever) y la capa de configuración maneja la complejidad.

Mira el PR #56404 para la implementación.