Organized Configuration in Rails

When building any Rails (SaaS) app, I often need to manage various configuration settings across different environments. Rails provides a powerful built-in feature through Rails.application.config_for that lets you load environment-specific configurations from YAML files.

Rails configuration files support different scopes:

  • shared: settings that apply to all environments;
  • environment-specific: Settings that only apply to specific environments like development, production, or test.

It automatically merges the shared section with the current environment’s settings, that gives you a clean way to define common defaults while allowing environment-specific overrides.

While this is all nice. I don’t like the API all too much. To use it you have to write something like Rails.application.config.bot.api_key. Ugh! That can be done better. Let’s look at how I have set this up in the past (and current) apps I built.

You can also check out the GitHub repo for the full implementation.

First a dedicated folder for all business-related configuration:

mkdir config/configurations

Now let’s create a thin wrapper around Rails’ configuration system in lib/configuration.rb (files in my lib/ folder are/should be copy-pasteable throughout my apps):

module Config
  module_function

  def load!
    settings_path = Rails.root.join("config", "configurations")

    return unless File.directory?(settings_path)

    Dir.glob(settings_path.join("*.yml")).each do |path|
      file_name = File.basename(path, ".yml")

      const_set(
        file_name.camelize,
        Rails.application.config_for("configurations/#{file_name}")
      )
    end
  end
end

This simple module automatically loads all YAML files from your config/configurations directory and creates constants based on the filename. For example, bot.yml becomes Config::Bot.

To enable this system, add the following to your config/application.rb:

require_relative "../lib/configuration"

module YourApp
  class Application < Rails::Application
    config.load_defaults 8.0

    Config.load!

    # … rest of your configuration
  end
end

Here’s an example bot configuration in config/configurations/bot.yml:

shared:
  api_key: <%= ENV.fetch("BOT_API_KEY", Rails.application.credentials.dig(:bot, :api_key)) %>
  user_agent: "MyAwesomeBot/1.0"
  timeout: 10

production:
  endpoint: <%= ENV.fetch("BOT_ENDPOINT", "https://bot.mybot.com") %>

development:
  endpoint: <%= ENV.fetch("BOT_ENDPOINT", "http://localhost:3000/bot") %>

I set up most keys to first fetch the environment for a key that follows the convention of the filename as a prefix. So for above example if I set BOT_API_KEY it will use that instead of the fallback defined in Rails.application.credentials.dig(:bot, :api_key).

You can now access these settings anywhere (including in initializers!) in your app:

Config::Bot.api_key
Config::Bot.endpoint
Config::Bot.timeout

Much better, right?! ❤️

Here are some other configuration examples:

Email settings (config/configurations/email.yml):

shared:
  provider: <%= ENV.fetch("EMAIL_PROVIDER", "logger") %> # using the Courrier gem

production:
  provider: <%= ENV.fetch("EMAIL_PROVIDER", "postmark") %>
  api_key: <%= ENV.fetch("EMAIL_API_KEY", Rails.application.credentials.dig(:mailpace, :api_key)) %>

Stripe (config/configuration/stripe.yml):

shared:
  api_key: <%= ENV.fetch("STRIPE_API_KEY", Rails.application.credentials.dig(:stripe, :api_key)) %>
  api_version: <%= ENV.fetch("STRIPE_API_VERSION", "2025-07-30.basil") %>
  max_network_retries: <%= ENV.fetch("STRIPE_MAX_NETWORK_RETRIES", 2) %>

development:
  default_price_id: price_in_development

production:
  default_price_id: price_in_production
  signing_secret_key: <%= ENV.fetch("STRIPE_SIGNING_KEY", Rails.application.credentials.dig(:stripe, :signing_secret)) %>

This approach gives a clean, organized way to manage all business logic’s configuration that has served me well. The automatic ENV variable fallbacks following the FILENAME_SETTING convention make it easy to override settings in different environments without touching your code.

What do you think of this solution?

Published at . Have suggestions or improvements on this content? Do reach out.