Rails credentials are not a replacement for configuration
You're adding Stripe to a Rails app. Nothing fancy, just a few settings:
yamlstripe:publishable_key: pk_test_...secret_key: sk_test_...webhook_secret: whsec_...checkout_success_path: /billing/successcheckout_cancel_path: /billing
The first three values are secrets. The last two are not.
Once you've opened bin/rails credentials:edit, though, it's very tempting to put the whole block in there and move on.
The app boots. The file is encrypted. The pull request doesn't expose anything awkward.
That's where the trouble starts: the encrypted file no longer contains only secrets. It also contains product behaviour, routes, public keys, and little operational choices that someone may need to review later.
Rails credentials solve one problem: keeping secrets out of plain text. They don't solve the broader problem of making application configuration understandable, reviewable, and easy to change.
Those are different jobs.
The credentials junk drawer
I like Rails credentials. They're a huge improvement over copying API keys between wiki pages, password managers, and forgotten .env files. Rails gives you an encrypted config/credentials.yml.enc, keeps the master key out of Git, and exposes the values through Rails.application.credentials.
Good machinery.
The trouble starts when every setting begins to look secret-shaped:
yamlstripe:secret_key: sk_live_...webhook_secret: whsec_...statement_descriptor: ACMEtrial_days: 14plans:starter: price_123business: price_456
Some of that belongs there. Some of it doesn't.
The trial length is product behaviour. The plan mapping might be business configuration. The statement descriptor is visible to customers. If all of that lives inside an encrypted file, the interesting change disappears from the review:
diff- trial_days: 14+ trial_days: 30
That is not a secret. That's a product decision.
When it lives in credentials, nobody sees it in a pull request. Nobody comments on it. Staging and production can drift apart for weeks, and the first person to notice might be a customer.
That's how a security feature turns into a junk drawer.
Secrets have a smell
These days I ask a simpler question first:
If a value would hurt you when it leaks, it belongs in credentials or another secret store. If a value only changes how the application behaves, it probably belongs somewhere visible.
That makes these good candidates for credentials:
- API secret keys
- webhook signing secrets
- OAuth client secrets
- encryption keys
- passwords for external services
And these usually deserve a different home:
- feature flags
- retry counts
- public API keys
- price identifiers
- route names
- copy, labels, and email addresses shown to users
- timeouts that explain product behaviour
There are fuzzy cases. A Stripe price ID isn't a password, but maybe you don't want it casually exposed. A public key is called a key, but the whole point is that it gets sent to the browser.
So I don't start with "can Rails credentials store this?"
They can. The better question is "should this change be reviewable?"
Use plain configuration for plain facts
Rails already gives us a few places for ordinary configuration.
For small, code-shaped values, config.x is often enough:
ruby# config/application.rbconfig.x.billing.trial_days = 14config.x.billing.success_path = "/billing/success"config.x.billing.cancel_path = "/billing"
Then your code reads the setting without pretending it's secret:
rubyredirect_to Rails.configuration.x.billing.success_path
For larger configuration, a YAML file can be clearer:
yaml# config/billing.ymlshared:trial_days: 14development:stripe_publishable_key: pk_test_...success_path: /billing/successproduction:stripe_publishable_key: pk_live_...success_path: /billing/success
Load it with config_for:
ruby# config/application.rbconfig.x.billing = config_for(:billing)
Now the ordinary settings show up as ordinary code review material:
rubyRails.configuration.x.billing["trial_days"]
The actual secret can stay in credentials:
rubyStripe.api_key = Rails.application.credentials.dig(:stripe, :secret_key)
The split looks boring, which is exactly the point. Secrets go where secrets go. Configuration stays where humans can see it.
Environment variables still have a place
There's another trap here: turning the credentials file into the only source of runtime truth.
Credentials are great for values you want to deploy with the app. Environment variables are still useful for values owned by the environment:
rubyconfig.x.app.host = ENV.fetch("APP_HOST")config.x.worker.concurrency = ENV.fetch("WORKER_CONCURRENCY", 5).to_i
The hostname depends on where the app runs. Worker concurrency may depend on the machine size. A one-off review app might need a different callback URL.
Encryption doesn't make those values better. It makes them harder to operate.
I don't want to redeploy encrypted credentials because production moved to a larger instance. I want the environment to tell the app what the environment is.
Make missing configuration loud
The worst configuration bug is the one that quietly falls back to something plausible.
This is especially true when secrets and non-secrets meet:
rubyStripe.api_key = Rails.application.credentials.dig(:stripe, :secret_key)
If that returns nil, the app may boot happily and fail later, somewhere far away from the mistake.
Make required values explicit:
rubyStripe.api_key =Rails.application.credentials.dig(:stripe, :secret_key) ||raise("Missing stripe.secret_key in Rails credentials")
For environment variables, ENV.fetch does the same kind of work:
rubyRails.application.routes.default_url_options[:host] = ENV.fetch("APP_HOST")
A boot-time crash is annoying for about thirty seconds. Finding the same mistake through a broken checkout flow is worse.
A small decision table
When I'm unsure where something belongs, I run through this:
Would leaking this value create a security problem? Put it in credentials or a secret manager.
Would I want to review a change to this value in a pull request? Keep it in code or a plain config file.
Does the hosting environment own this value? Read it from ENV.
Is it really product behaviour? Give it a name in the codebase where the team can find it.
That last one is the easy one to miss. Product behaviour loves disguising itself as configuration:
- trial length
- retry limits
- allowed file sizes
- billing plan mappings
They look like knobs, but they often encode decisions. I want those changes in a diff where someone can ask why.
The boring split
My Rails apps are calmer when the split looks roughly like this:
ruby# SecretRails.application.credentials.dig(:stripe, :secret_key)# Environment-ownedENV.fetch("APP_HOST")# Application-ownedRails.configuration.x.billing.trial_days
Credentials are not bad. They're easy to overuse because they make awkward things disappear.
But the awkwardness is often the point.
If a value changes behaviour, I want the team to see it when it changes.
Rails gives us enough places to put each kind of value. The trick is not using encrypted credentials for everything.