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)