loading...
Cover image for Easier usage of Rails 5.2 credentials and app-specific configuration

Easier usage of Rails 5.2 credentials and app-specific configuration

svyatov profile image Leonid Svyatov ・3 min read

As you all know by now, Rails 5.2 introduced a new feature called Credentials. DHH said the following in the PR:

This new file is a flat format, not divided by environments like secrets.yml has been. Most of the time, these credentials are only relevant in production, and if someone does need to have some keys duplicated for different environments, it can be done by hand.

And as you should know by now, Rails 6.0 fixes this obvious inconvenience.

Meantime, a commonly proposed solution for Rails 5.2 was just to add environments by hands, something like so:

development:
  facebook_app_id: '...'
  facebook_app_secret: '...'
  facebook_app_namespace: '...'
  stripe_publishable_key: '...'
  stripe_secret_key: '...'
  stripe_signing_secret: '...'

production:
  facebook_app_id: '...'
  facebook_app_secret: '...'
  facebook_app_namespace: '...'
  stripe_publishable_key: '...'
  stripe_secret_key: '...'
  stripe_signing_secret: '...'

And then use it like this:

Rails.application.credentials[Rails.env.to_sym][:facebook_app_secret]

Pretty long and not very readable, right? Yeah, you probably don’t use credentials in that many places in your app anyway, but still. This line has 47 characters about Rails and just 22 characters about what you need. Not the best ratio, in my opinion.

The solution I’d like to show you is quite obvious, but I’ve never seen it being suggested anywhere (maybe I didn’t search right enough, sorry if that’s the case).

Just open config/application.rb and add credentials method like so:

require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module YourApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end

  def self.credentials
    @credentials ||= Rails.application.credentials[Rails.env.to_sym]
  end
end

Let’s see what it changes:

# Before:
Rails.application.credentials[Rails.env.to_sym][:facebook_app_secret]

# After
YourApp.credentials[:facebook_app_secret]

This approach abstracts Rails-specific details away, makes it concise, expressive and about your application. As a bonus it gives you a lot of flexibility: you can replace Rails credentials with something else, you can merge credentials from different places, you can easily upgrade to Rails 6 credentials by removing [Rails.env.to_sym] part, and so on.

OK, that’s great, but post’s title was also saying something about “app-specific configuration,” right? Correct! I want to show you one more thing that can make your life with Rails a little easier.

As your Rails application grows sooner or later, you will need a place to store various environment-specific details for your app: things that aren’t secrets or credentials, just some custom configuration. Rails documentation even has a section with this exact name: Custom configuration.

According to the documentation Rails offers us two pretty powerful options:

  1. config.x namespace
  2. config_for method

Neat! How about we use our credentials approach described above and combine with these two options?

Step 1: create config/app.yml

shared: &shared
  facebook_url: 'https://www.facebook.com/mysite'
  twitter_url: 'https://twitter.com/mysite'

development:
  <<: *shared
  domain: 'localhost:3000'
  devise_mailer_sender: 'no-reply@localhost'
  orders_mailer_sender: 'no-reply@localhost'

production:
  <<: *shared
  domain: 'mysite.com'
  devise_mailer_sender: 'no-reply@mysite.com'
  orders_mailer_sender: 'orders@mysite.com'

Step 2: fill in config.x from app.yml in the config/application.rb

# App specific configuration
config.x = config_for(:app).with_indifferent_access

Step 3: add config method to the config/application.rb file

  def self.config
    @config ||= Rails.configuration.x
  end

So your config/application.rb file now looks like this:

require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module YourApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.

    # App-specific configuration
    config.x = config_for(:app).with_indifferent_access
  end

  def self.config
    @config ||= Rails.configuration.x
  end

  def self.credentials
    @credentials ||= Rails.application.credentials[Rails.env.to_sym]
  end
end

And we can use our custom configuration like so:

YourApp.config[:devise_mailer_sender]

How cool is that? :)

Posted on by:

svyatov profile

Leonid Svyatov

@svyatov

Web apps developer, lifelong learner, problem solver, solution finder, technologist, and photography enthusiast.

Discussion

pic
Editor guide
 

I currently don't use Rails, but when I did I usually added a custom class named ApplicationConfig (to follow Rails 5 conventions) and then wrapped Figaro/Secrets/Credentials/whatever in that.

This had a very similar result (e.g. ApplicationConfig[:devise_mailer_sender]) but also made it easier to swap out the actual credential storage or even have more than one for different types of credentials etc., without having to change the actual application code.

You can see an example of this in the dev.to codebase, where Mac added it after I suggested this approach previously:

github.com/thepracticaldev/dev.to/...

 

Great article. I was also confused how to use ENV variables as I did it before with secrets.yml. As a result I created a gem for easy credentials management in Rails 5.2. Hope this will be useful! rubygems.org/gems/r_creds

 

You can also do something like this (if you had an environment variable set to SMTP_USERNAME):

user_name: Rails.application.credentials.SMTP_USERNAME
password:  Rails.application.credentials.SMTP_PASSWORD

You can use the line Rails.application.credentials.[YOUR ENV VARIABLE] to call your env variable you set in the credentials file.

That, to me, seems to be the easiest with the least amount of code needing to be written.

 

Thanks @Leonid for this wonderful approach for handling configurations.

I faced a problem in extending this approach to an already existing project.

Scenario:

  • There were existing config.x nested attributes in env specific files (eg: development.rb)
  • Defining self.config method in application.rb disrupts the default config.x class and thus affects the behaviour in other files (because now the config.x will be an instance of Hash/with_indifferent_access)
  • Also after defining the self.config, we will loose existing config. values

I worked around it by adding the following method.

config_for(:app).each do |key, val|
  config.send("#{key}=", val)
end

So that the existing behaviours is not disturbed.
Also I believe it's advised to use config.x only for nested attributes.