Rails 8.2 introduces Rails.app.creds for unified credential management

· 3 min read

Applications often store secrets in both environment variables and encrypted credential files. Migrating between these storage methods or using both simultaneously has traditionally required code changes. Rails 8.2 solves this with Rails.app.creds, a unified API that checks ENV first, then falls back to encrypted credentials.

Before

Managing credentials from multiple sources meant mixing different APIs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class StripeService
  def initialize
    # Check ENV first, fallback to credentials
    @api_key = ENV["STRIPE_API_KEY"] || Rails.application.credentials.dig(:stripe, :api_key)
    @webhook_secret = ENV.fetch("STRIPE_WEBHOOK_SECRET") {
      Rails.application.credentials.stripe&.webhook_secret
    }

    raise "Missing Stripe API key!" unless @api_key
  end
end

class DatabaseConfig
  def connection_url
    # Different syntax for each source
    ENV["DATABASE_URL"] || Rails.application.credentials.database_url
  end

  def redis_url
    ENV.fetch("REDIS_URL", Rails.application.credentials.dig(:redis, :url) || "redis://localhost:6379")
  end
end

This approach has several problems:

  • Inconsistent APIs between ENV.fetch() and credentials.dig()
  • Manual fallback logic scattered throughout the codebase
  • Code changes required when moving secrets between storage methods
  • Easy to forget nil checks on nested credentials

Rails 8.2

The new Rails.app.creds provides a consistent interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class StripeService
  def initialize
    @api_key = Rails.app.creds.require(:stripe_api_key)
    @webhook_secret = Rails.app.creds.require(:stripe_webhook_secret)
  end
end

class DatabaseConfig
  def connection_url
    Rails.app.creds.require(:database_url)
  end

  def redis_url
    Rails.app.creds.option(:redis_url, default: "redis://localhost:6379")
  end
end

The require method mandates a value exists and raises KeyError if missing from both ENV and encrypted credentials. The option method returns nil or a default value gracefully.

Nested Keys

For nested credentials, pass multiple keys. Rails automatically converts them to the appropriate format for each source:

1
2
3
4
5
6
# Checks ENV["AWS__ACCESS_KEY_ID"] first, then credentials.dig(:aws, :access_key_id)
Rails.app.creds.require(:aws, :access_key_id)

# Multi-level nesting
# ENV["REDIS__CACHE__TTL"] || credentials.dig(:redis, :cache, :ttl)
Rails.app.creds.option(:redis, :cache, :ttl, default: 3600)

The ENV lookup uses double underscores (__) as separators for nested keys:

  • :database_urlENV["DATABASE_URL"]
  • [:aws, :region]ENV["AWS__REGION"]
  • [:redis, :cache, :ttl]ENV["REDIS__CACHE__TTL"]

Dynamic Defaults

The option method accepts callable defaults, evaluated only when needed:

1
2
Rails.app.creds.option(:cache_ttl, default: -> { 1.hour })
Rails.app.creds.option(:max_connections, default: -> { calculate_pool_size })

ENV-Only Access

Access environment variables directly using the same API via Rails.app.envs:

1
2
3
# Only checks ENV, no encrypted credentials fallback
Rails.app.envs.require(:port)
Rails.app.envs.option(:log_level, default: "info")

Custom Credential Sources

Under the hood, Rails.app.creds is powered by ActiveSupport::CombinedConfiguration, which checks multiple credential sources (called backends) in order. By default, it checks ENV first, then encrypted credentials. You can customize this chain to include external secret managers:

1
2
3
4
5
6
7
# config/initializers/credentials.rb
Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
  Rails.app.envs,                   # Check ENV first
  VaultConfiguration.new,           # Then HashiCorp Vault
  OnePasswordConfiguration.new,     # Then 1Password
  Rails.app.credentials             # Finally, encrypted credentials
)

Each credential source needs to implement require and option methods matching the API.

Rails.app Alias

This feature comes alongside a new Rails.app alias for Rails.application:

1
2
3
4
5
# Before
Rails.application.credentials.aws.access_key_id

# After
Rails.app.credentials.aws.access_key_id

The shorter alias makes chained method calls more pleasant to read and write.

Conclusion

Rails.app.creds eliminates the friction of managing credentials across multiple sources. Secrets can move between ENV and encrypted files without touching application code.

References

  • PR #56404 - Add Rails.app.creds for combined credentials lookup
  • PR #56403 - Add Rails.app alias for Rails.application
Prateek Choudhary
Prateek Choudhary
Technology Leader