This article was originally published on Build a SaaS with Rails from Rails Designer
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, ortest.
Rails 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:
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
Here are some other useful configuration examples:
Email settings (config/configurations/email.yml):
shared:
  provider: <%= ENV.fetch("EMAIL_PROVIDER", "logger") %>
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?
 

 
    
Top comments (0)