Organized Configuration in Rails

This set up, along with a generator (bin/rails generate config bot api_key user_agent timeout), is also part of Kern, SaaS starter for Rails apps.

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/config.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 "./lib/config"

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?

Rails 8.2 will introduce Rails.app.creds that adds an API like Rails.app.creds.{require,options}(:bot_api_key). This might be a better solution as it needs less set up, but a more verbose syntax.

Product-minded Rails notes

Once a month: straightforward notes on improving UX in Rails—what to simplify, what to measure, and UI/frontend changes that move real usage.

Over to you…

What did you like about this article? Learned something knew? Found something is missing or even broken? 🫣 Let me (and others) know!

Comments are powered by Chirp Form

Want to read me more?