5 Unexpected Ways to Use Rails.app.creds
Rails 8.2 introduced Rails.app.creds as a unified way to access credentials from ENV and encrypted files. But buried in the implementation is something more powerful: CombinedConfiguration is a chain of responsibility for configuration. Each backend either returns a value or passes to the next.
This isn’t just credential lookup. It’s composable configuration middleware.
Here are five patterns that exploit this architecture.
1. Feature Flags Without a Service
Most apps don’t need LaunchDarkly. You need a way to toggle features that’s version-controlled and overridable in development.
# config/credentials.yml.enc
features:
new_checkout: false
dark_mode: true
ai_summaries: false
Access them like any credential:
if Rails.app.creds.option(:features, :new_checkout, default: false)
render_new_checkout
else
render_legacy_checkout
end
The magic: ENV overrides credentials. Enable a feature locally without touching the encrypted file:
FEATURES__NEW_CHECKOUT=true bin/rails server
Need to test a feature in staging? Set the ENV var in your deployment config. No code changes, no credentials edit, no deploy.
Making it ergonomic
Wrap it in a 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
# Usage
<% if feature?(:new_checkout) %>
<%= render "checkout/new" %>
<% end %>
You now have feature flags that are:
- Version controlled (in credentials)
- Environment-overridable (via ENV)
- Zero external dependencies
- Free
2. Developer Override Files
Every team has this problem: “How do I test with different credentials locally without committing them?”
Create a gitignored local override file:
# 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, "Missing: #{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
# Insert at highest priority
if Rails.env.development?
Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
DeveloperOverrides.new,
Rails.app.envs,
Rails.app.credentials
)
end
Add to .gitignore:
.secrets.local.yml
Now any developer can create their own override file:
# .secrets.local.yml (gitignored)
stripe:
secret_key: sk_test_my_personal_test_key
openai:
api_key: sk-my-own-openai-key
features:
experimental_ui: true
No more “I accidentally committed my API key” incidents. No more “let me temporarily edit credentials.yml.enc”. Each developer has their own sandbox.
3. Request-Scoped Configuration
What if admins could override configuration per-request? Useful for debugging, customer support, or A/B testing.
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :config_overrides
end
# lib/request_scoped_configuration.rb
class RequestScopedConfiguration
def require(*keys)
option(*keys) # Always returns nil to continue chain if not found
end
def option(*keys, default: nil)
Current.config_overrides&.dig(*keys)
end
def keys
Current.config_overrides&.keys || []
end
def reload
# No-op, cleared per request
end
end
Wire it up:
# config/initializers/credentials.rb
Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
RequestScopedConfiguration.new,
Rails.app.envs,
Rails.app.credentials
)
Now in a controller:
class Admin::DebugController < ApplicationController
before_action :require_admin
def impersonate_config
# Temporarily use Stripe test mode for this admin session
Current.config_overrides = {
stripe: { test_mode: true },
features: { debug_panel: true }
}
redirect_back fallback_location: root_path, notice: "Debug mode enabled"
end
end
The rest of your app doesn’t know or care. It just calls Rails.app.creds.option(:stripe, :test_mode) and gets the right value for the current request.
4. Audit Logging Wrapper
Compliance requirements often mandate logging access to sensitive credentials. With CombinedConfiguration, you can wrap any backend with 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
Wrap your production credentials:
# 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
Every credential access is now logged:
{"event":"credential_access","method":"require","keys":"stripe.secret_key","file":"app/services/payment_service.rb","line":42,"timestamp":"2026-01-05T10:30:00Z"}
You can feed this to your SIEM, set up alerts for unusual access patterns, or satisfy auditors who want proof of access controls.
5. Scheduled Configuration
What if configuration could change based on time? Black Friday pricing. Holiday themes. Scheduled maintenance windows.
# 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 } # Shorter cache during high traffic
}
),
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, "Missing: #{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
# Could reload from database or external source
end
end
Wire it into the chain:
Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
ScheduledConfiguration.new, # Time-based overrides first
Rails.app.envs,
Rails.app.credentials
)
On Black Friday, Rails.app.creds.option(:pricing, :discount_percent) automatically returns 30. No deploy needed. When the schedule ends, it falls through to the default.
Your pricing code doesn’t need to know about Black Friday:
def apply_discount(price)
discount = Rails.app.creds.option(:pricing, :discount_percent, default: 0)
price * (1 - discount / 100.0)
end
The Pattern
All five examples share the same insight: CombinedConfiguration is middleware for configuration. The chain of responsibility pattern lets you:
- Layer behaviors without modifying existing code
- Compose features by ordering backends
- Separate concerns between storage, access control, and business logic
The order matters:
ActiveSupport::CombinedConfiguration.new(
RequestScopedConfiguration.new, # Highest priority: per-request overrides
ScheduledConfiguration.new, # Time-based rules
DeveloperOverrides.new, # Local development
Rails.app.envs, # Environment variables
AuditedConfiguration.new( # Wrapped for logging
Rails.app.credentials
)
)
First non-nil value wins. Each backend can either handle the request or pass it down the chain.
Wrapping Up
Rails.app.creds looks like a simple convenience API. But CombinedConfiguration is a building block for sophisticated configuration systems:
- Feature flags without external services
- Developer sandboxes without credential conflicts
- Request-scoped debugging
- Compliance-ready audit logging
- Time-based configuration without deploys
The best part? Your application code stays clean. It just calls Rails.app.creds.option(:whatever) and the configuration layer handles the complexity.
See PR #56404 for the implementation.