1Password as the Source of Truth
A small Rails/Heroku secrets system that made rotation less dramatic.
Welcome back to In General: one useful thing I tried, built, or figured out this week. AI workflows, productivity systems, dev tools, side-project mechanics. All road-tested before they show up here. If someone forwarded this to you, subscribe here.
Every real app eventually becomes a small museum of API tokens.
Stripe keys. S3 keys. OAuth client secrets. LLM provider tokens. Webhook signing secrets. Database URLs. Encryption keys. Development needs one set. Staging needs another. Production needs the real ones.
That is manageable until rotation stops being theoretical. A vendor gets breached. A key expires. A SOC 2 checklist appears. Someone leaves a project. A token shows up in the wrong log. Suddenly, the question is not “where did we put this secret?” It is “how do we change it safely without breaking everything?”
The part I have been working on is how to avoid creating another mess.
The useful move is not just “put secrets in 1Password.” The useful move is making 1Password the one human-editable source of truth, then scripting every edge where those secrets need to appear.
The boring shape
I built this around four pieces: a 1Password vault schema, a seeder, a Heroku pusher, and an expiry checker.
The vault is named like Company-ENV. Inside it, there is one Secure Note per service: stripe, aws, google, llm, rails, sentry, and so on. Each item has development and production sections. Each section has concealed fields plus an optional expires date.
The references look like this:
op://Company-ENV/stripe/production/api_key
op://Company-ENV/aws/development/access_key_id
op://Company-ENV/rails/production/secret_key_base
Those labels are load-bearing. Lowercase. No spaces. If someone renames api_key to API Key in the 1Password app, every script that reads the reference breaks.
So bin/op-seed-vault defines the canonical shape. It creates missing items, sections, fields, and expiry dates. It never overwrites existing values.
bin/op-seed-vault --dry-run
bin/op-seed-vaultAfter that, the human work happens where it should: in the 1Password app.
Heroku is a downstream state
The Rails app does not read from 1Password at runtime. I do not want production boot to depend on a password manager API call.
Heroku config vars remain the runtime interface. The difference is that nobody edits them by hand anymore. bin/push-secrets-to-heroku reads a committed .env.prod.tpl manifest full of op:// references, resolves them through the 1Password CLI, and pushes the resulting values to Heroku in one batch.
bin/push-secrets-to-heroku --app company-staging --dry-run
bin/push-secrets-to-heroku --app company-production --dry-runThat creates a clean rule: 1Password is canonical. Heroku is a snapshot.
Rotation becomes: update the value in 1Password, push to staging, smoke-test, push to production, restart the app if boot-time config needs to reload.
Rails keeps its ergonomics through Rails.app.creds, backed by a CombinedConfiguration layer that resolves ENV first and falls back to encrypted Rails credentials where that still makes sense.
Rails.app.creds.require(:stripe, :api_key) # ENV["STRIPE__API_KEY"]
Rails.app.creds.option(:sentry, :dsn) # ENV["SENTRY__DSN"]In production, config.require_master_key = false. The app can boot without master.key because production secrets arrive through ENV.
The tradeoff
This is not HashiCorp Vault. It does not issue dynamic database credentials. It does not give every process just-in-time ephemeral access. It still leaves deployed values in Heroku config vars.
For a larger team, a heavier compliance environment, or a Kubernetes/AWS-native stack, I would look closely at a dedicated secrets manager.
For a Rails app on Heroku, this feels like the right shape. I’m already using 1Password all the time. Heroku is where the app expects the runtime config. Rails already has a decent access pattern. The missing part was the connective tissue.
The one unattended piece is bin/secret-expiry, which can run with a read-only, vault-scoped 1Password service account via OP_SERVICE_ACCOUNT_TOKEN. It reads expiry dates se in 1Password, not secret values, and reports what needs attention
A secret you cannot rotate confidently is not really managed. It is just encrypted somewhere.
Secrets management is about making change safe later.



