Skip to content

As your Ruby on Rails application grows, you‘ll need to add your own bits of configuration.

Where do you put API keys for third-party apps? What if you need different application values depending on the Rails environment? What if you‘re testing a new feature that should be disabled in production for the time-being? Sure, you can add some ENV vars here and there. You could manually load YAML files in different places throughout your app. You might be tempted to reach for another Ruby gem to help manage all this behind a common interface.

But there‘s no need for all that. Rails.configuration has your back. It‘s got features to support your custom configuration, not to mention the built-in X-factor: config.x.

Link to headingThe Ins and Outs

You can set any value you want on Rails.configuration:

ruby
Rails.configuration.newsletter_title = "Joy of Rails"
Rails.configuration.newsletter_title # => "Joy of Rails"

Within the scope of YourApp::Application (replace "YourApp" with your app‘s namespace), you can access Rails.configuration through the config method:

config/application.rb
module YourApp
  class Application < Rails::Application
    config # => #<Rails::Application::Configuration:0x000...>

    config.newsletter_title = "Joy of Rails"
    config.newsletter_title # => "Joy of Rails"
  end
end

You can also use the config method under Rails.application.configure, such as in an initializer file:

config/initializers/my_setting.rb
Rails.application.configure do
  config # => #<Rails::Application::Configuration:0x000...>

  config.newsletter_title = "Joy of Rails"
end

Everywhere else, use Rails.configuration.

ruby
Rails.configuration.newsletter_title # => "Joy of Rails"

Link to headingUsing Rails config.x

You‘ll often want to group related values together. For this purpose, Rails config.x offers a clean and consistent namespace for storing your custom configuration settings.

Here’s a simple example. Note the format config.x.nested.value:

config/application.rb
module YourApp
  class Application < Rails::Application
    config.x.payment_gateway.merchant_id = 'ABC123'
    config.x.payment_gateway.enabled = true
  end
end

Add logic to assignment:

config/application.rb
module YourApp
  class Application < Rails::Application
    config.x.cache.ttl = Rails.env.production? ? 1.hour : 5.minutes
  end
end

Load values from Rails credentials or from ENV variables with defaults:

config/application.rb
module YourApp
  class Application < Rails::Application
    config.x.analytics.api_secret = credentials.dig(:analytics, :api_secret) || 'TEST_KEY'

    config.x.analytics.provider = ENV.fetch('ANALYTICS_PROVIDER', 'google')
  end
end

Type cast from string inputs:

config/application.rb
module YourApp
  class Application < Rails::Application
    # Boolean configuration
    config.x.analytics.enabled = ActiveModel::Type::Boolean.new.cast(ENV.fetch('ANALYTICS_ENABLED', 'true'))

    # Numeric configuration
    config.x.rate_limiting.max_requests = ENV.fetch('MAX_REQUESTS', '100').to_i
  end
end

Access the values through Rails.configuration.x anywhere in your application:

app/controllers/payments_controller.rb
class PaymentsController < ApplicationController
  def create
    gateway = PaymentGateway.new(
      merchant_id: Rails.configuration.x.payment_gateway.merchant_id
    )

    # Use feature toggle
    if Rails.configuration.x.features.enable_new_ui
      render 'new_payment_form'
    else
      render 'legacy_payment_form'
    end
  end
end
app/clients/analytics_client.rb
class AnalyticsClient
  def track(event)
    return unless Rails.configuration.x.analytics.enabled

    provider = Rails.configuration.x.analytics.provider
    # Use the provider to track the event
  end
end

Link to headingLoading Configuration from YAML Files

You may find it convenient to describe a group of related settings in YAML files in the config directory as Rails does for database configuration with config/database.yml.

Rails provides the Rails.application.config_for method to load settings from YAML files:

config/analytics.yml
default: &default
  enabled: true

development:
  <<: *default
  provider: 'local'
  sampling_rate: 100

production:
  <<: *default
  provider: 'google'
  sampling_rate: 10
  property_id: 'UA-XXXXXXXX-X'

Load the configuration in your application such as within the context of Rails::Application during the boot process like so:

ruby
module YourApp
  class Application < Rails::Application
    config.analytics = config_for(:analytics)
  end
end

With this approach, you have created your own config namespace under Rails.configuration.analytics. Now you can access the configuration anywhere:

ruby
Rails.configuration.analytics.enabled
Rails.configuration.analytics.provider

The config_for method will expect to find settings under the current Rails.env so make sure to organize your YAML configuration accordingly.

Link to headingLet’s get practical

In case you‘re having trouble imagining when you would reach for Rails custom configuration, here are just a few practical use cases.

Link to headingBare bones feature toggles

Feature toggles allow you to enable or disable features based on the environment:

config/environments/development.rb
Rails.application.configure do
  config.x.features.enable_beta = true
  config.x.features.enable_api_v2 = true
end
config/environments/production.rb
Rails.application.configure do
  config.x.features.enable_beta = false
  config.x.features.enable_api_v2 = false
end
app/controllers/your_controller.rb
if Rails.configuration.x.features.enable_beta
  # Render beta feature
end

Link to headingConfiguring VAPID web push

If you‘d like to use Web Push in your Rails app, load your "Voluntary Application Server Identification" (VAPID) public and private keys from your Rails credentials.

config/initializers/vapid.rb
Rails.application.configure do
  config.x.vapid.public_key = credentials.dig(:vapid, :public_key)
  config.x.vapid.private_key = credentials.dig(:vapid, :private_key)
end

Link to headingExternal Service Configuration

When working with external services, you often need different settings for development and production:

config/smtp.yml
development:
  address: 'localhost'
  port: 1025
  domain: 'localhost.test'

production:
  address: 'smtp.sendgrid.net'
  port: 587
  domain: 'yourdomain.com'
  user_name: <%= ENV['SENDGRID_USERNAME'] %>
  password: <%= ENV['SENDGRID_PASSWORD'] %>
config/application.rb
module YourApp
  class Application < Rails::Application
    config.action_mailer.smtp_settings = config_for(:smtp)
  end
end

Link to headingGetting to know ActiveSupport::OrderedOptions

The secret sauce behind Rails configuration is ActiveSupport::OrderedOptions. When you call a new method on config.x for the first time, an instance of ActiveSupport::OrderedOptions is lazy-initialized.

ruby
Rails.configuration.x.anything.class # => ActiveSupport::OrderedOptions

You may recognize ActiveSupport::OrderedOptions as the underlying mechanism that powers the configuration of most Rails gems it‘s used extensively in Rails’ default configurations (e.g., config.action_mailer, config.active_record).

And nothing is stopping you from manually using ActiveSupport::OrderedOptions for your custom configuration namespaces.

ruby
Rails.configuration.aws = ActiveSupport::OrderedOptions.new
Rails.configuration.aws.access_key_id = credentials.dig(:aws, :access_key_id)
Rails.configuration.aws.secret_access_key = credentials.dig(:aws, :secret_access_key)

ActiveSupport::OrderedOptions is a subclass of Hash with a little syntax sugar baked in. Here are some more examples of how to use it:

ruby
require 'active_support/ordered_options'

config = ActiveSupport::OrderedOptions.new

# Set values using hash syntax or method calls
config[:api_key] = '12345'
config.max_users = 100

# Access values using hash syntax or method calls
puts config[:api_key]  # => 12345
puts config.max_users  # => 100

# Check for key existence
puts config.has_key?(:api_key)  # => true

# Nested configurations
config.database = ActiveSupport::OrderedOptions.new
config.database.username = credentials.dig(:db, :username)
config.database.password = credentials.dig(:db, :password)

puts config.database.username  # => admin

Link to headingRaising exceptions for missing values

One powerful feature of ActiveSupport::OrderedOptions is the ability to raise an exception when accessing a value that is nil or blank by adding a ! to the end of the method name. This is particularly useful for catching configuration errors early:

ruby
settings = ActiveSupport::OrderedOptions.new

settings.required_api_key! # => Raises KeyError

This feature is incredibly useful for validating that all required configuration values are properly set, especially for critical settings like API keys or database credentials.

By raising an exception early, you can prevent your application from running with incomplete or invalid configuration, which could lead to errors or unexpected behavior later on.

config/initializers/my_api_client.rb
Rails.configuration.x.api_client.secret_key!

Link to headingOrderedOptions in the wild

ActiveSupport::OrderedOptions is used extensively throughout the framework.

ruby
Rails.application.configure do
  config.action_controller.class # => ActiveSupport::OrderedOptions
  config.action_mailer.class     # => ActiveSupport::OrderedOptions
  config.active_job.class        # => ActiveSupport::OrderedOptions
  config.active_record.class     # => ActiveSupport::OrderedOptions
  config.active_support.class    # => ActiveSupport::OrderedOptions
  # ... you get the point
end

On top of that, many of your favorite gems use it to provide you with the ability to customize behavior to fit the needs of your application.. If you look hard enough, you will find many examples of ActiveSupport::OrderedOptions for configuring Rails gems littered across the landscape. Here are just a few:

This search query on GitHub open source alone yields 4.5K results.

Link to headingHot tips

Armed with knowledge of the Rails features that enable custom configuration, here are some guidelines to keep in mind:

  1. Use a common interface: Custom configuration values can come from many sources: YAML files, ENV vars, Rails credentials—but don‘t embed knowledge of all these sources in your application logic. During the Rails initialization process, copy your settings into config.x. This approach makes access to custom configuration throughout your application consistent.

    ruby
    # All roads lead to config.x
    
    config.x.analytics = config_for(:analytics)
    
    config.x.payment.api_secret = credentials.dig(:payment, :api_secret)
    
    config.x.vapid.private_key = ENV.fetch("VAPID_PRIVATE_KEY")
    
  2. Namespace Your Configurations: Keep your configurations organized by using namespaces, preferably with config.x.

    ruby
    config.x.payment_gateway.timeout = 30
    config.x.payment_gateway.retries = 3
    
    config.x.api.version = "v1"
    config.x.api.base_url = "https://api.example.com"
    
  3. Initialize early: It may be tempting to lazy-initialize your custom configuration. You are also free to modify your custom configuration at any time. However, this can lead to unexpected behavior in production. I recommend running all the setup of your custom configuration during the Rails initialization process under separate config/initializers files. Treat values as read-only in your app logic.

  4. Use Strong Typing: Convert string values to appropriate types when possible.

    ruby
    config.x.rate_limiting.max = ENV.fetch('MAX_REQUESTS', '100').to_i
    
  5. Leverage Rails Environments: Utilize Rails' built-in environment-specific configuration files for settings that vary between environments.

  6. Validate Configuration: Assert important configuration values required for your application to run and catch errors early in the Rails initialization process.

  7. Use Rails credentials for Sensitive Data: Never commit sensitive information in plain text directly in application files. Instead, use Rails credentials to store these values and load them into config.x as described above.

    config/credentials.yml.enc
    aws:
      access_key_id: YOUR_ACCESS_KEY_ID
      secret_access_key: YOUR_SECRET_ACCESS_KEY_ID
    

Check out the Rails guides for more on using Rails credentials.

Link to headingHey, I like my $THIRD_PARTY_GEM!

There are quite a few Ruby gems for managing custom Rails configuration too:

If you like ’em, use ’em! There‘s nothing wrong with using third-party gems for custom configuration, especially if you want the extra features they provide.

Just consider that using Rails built-in custom configuration features means:

  • No Additional Dependencies
  • Consistency with Rails Conventions

You don’t have to add gems for custom configuration thus reducing your maintenance burden and keep your dependency list lean. Using Rails' built-in methods keeps your configuration consistent with other Rails conventions, making it easier for Rails developers to understand and maintain.

Link to headingConclusion

At its core, Rails configuration is about managing various settings that control how your application behaves in different environments. Effective use of configuration allows you to:

  1. Keep environment-specific settings separate from your code
  2. Store sensitive data (like API keys) securely
  3. Make your application more maintainable and flexible

With Rails.configuration, config.x, and ActiveSupport::OrderedOptions, you can customize your application settings to your heart‘s content.


If you liked this article, please feel free to share it and subscribe to hear more from me and get notified of new articles by email.

Did you find a bug or do you have questions about the content? You can send me an email, connect with me on Twitter, Github, Mastodon, and/or Linkedin.

Joy of Rails is open source on Github. Feel free to look through the code and contribute.

That does it for another glimpse into what’s possible with Ruby on Rails. I hope you enjoyed it.

Ice cream
Ice cream