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, ortest.
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.
Want to read me more?
-
Two products, one Rails codebase
Build multiple products from a single Rails codebase using variants and custom configuration class. -
Using Subdomains in Rails: Development vs Production
A clean and simple approach to make work with subdomains locally. -
Rails UI Components Library Tips and Tricks
Rails Designer is a Rails UI components library. Next to the many components and variants, it comes packages with great quality of life features.
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
{{comment}}