A first-run setup wizard that refuses to run in production
September 17, 2026
The first ten minutes with a boilerplate are spent pasting keys into files you have not read yet.
A wizard is the obvious fix, and a wizard that writes secrets is also the most dangerous screen in
the application, so the interesting decisions are all about where it is allowed to exist.
Rails encrypted credentials are a file per environment: config/credentials/production.yml.enc,
decrypted by config/credentials/production.key, which is the thing you never commit. The workflow
Rails intends is bin/rails credentials:edit --environment production, an editor opens, you type
YAML, it re-encrypts on save.
Rails credentials per environment is the whole design, and that workflow is fine. It is also why
people give up on the first evening. You have to know the file exists, know the flag, know which
keys the application reads, and get the nesting right with no feedback until something boots
wrong. A wizard that lists the keys, shows which are missing and
writes the file is the same operation with the guesswork removed.
The important part is that the destination does not change. The wizard is a nicer editor for
credentials:edit, not a second configuration system. Nothing moves to the database, so nothing
about how the application reads its secrets is different because a wizard exists.
One before_action, applied to the whole controller rather than to the write actions, and that
breadth is the point. A deployed wizard would be readable as well as writable, and a read of that
screen is a list of exactly which secrets this installation holds.
Three things follow from the gate. A deployed console cannot rewrite a secret at runtime, so the
credentials in a running production app are whatever was committed and deployed, full stop. The
live checks, which call Stripe and send mail with a key somebody just typed into a browser, cannot
be triggered from the internet. And the wizard's navigation link is hidden rather than shown and
refused, so the deployed console does not advertise a door it will not open.
The read-only filesystem
Beyond the security argument there is one that would break the feature anyway. A production dyno on
Heroku, and a container almost anywhere else, has an ephemeral and effectively read-only
filesystem. Writing production.yml.enc there would either fail outright or succeed into a
filesystem that disappears on the next restart, taking the secrets with it.
Worse than both: a partial write. The encrypted file is rewritten whole, so a failure midway leaves
an unparseable file, and the next boot cannot read any credential rather than missing one. The
local gate removes the entire class of problem by never attempting the write anywhere it cannot
complete.
What a blank field has to mean
The form shows existing secrets masked, because showing them in full would put every key in the
browser history and in any screenshot the founder takes. Masked values create an ambiguity that has
to be resolved deliberately: when the field displays dots and the founder saves the step without
touching it, the submitted value is either the dots or nothing at all.
The rule is that a blank submit leaves the stored value alone. The alternative, treating blank as
an instruction to clear, means saving one step of the wizard silently empties every other key on
the page, and the founder finds out at the next boot with an application that cannot reach Stripe.
This is not a nicety. It is the difference between a wizard you can revisit to change one thing and
a wizard you can only ever fill in once, correctly, in a single pass.
Testing a key without lying about it
The wizard offers a live check: paste the Stripe secret key, press the button, get a yes or a no.
The naive implementation calls Stripe::Account.retrieve and reports the result, and it is
misleading in exactly one case.
Stripe.api_key was read once at boot. When you are configuring the environment the server is
currently running as, and the key in the box differs from the one in memory, a green tick would
confirm a key the application is not using. The guard asks for a restart instead.
For any other target, configuring production from your laptop, the running key is irrelevant and
the submitted key is tested directly. Same button, two meanings, and only one of them is a claim
about the running process.
What the wizard does not do
The wizard does not manage secrets over time. There is no rotation, no history, no record of who
changed what, because there is one founder and a git history of the encrypted file.
It also does not remove the deploy. Writing production.yml.enc locally is step one of three: the
file has to be committed and the application has to be deployed before production sees anything.
A wizard that made a change feel live when it was not would be worse than no wizard, so the
distinction is worth keeping in front of whoever is using it.
The first ten minutes with a boilerplate are spent pasting keys into files you have not read yet. A wizard is the obvious fix, and a wizard that writes secrets is also the most dangerous screen in the application, so the interesting decisions are all about where it is allowed to exist.
The admin hub covers what else the console holds.
Where a Rails secret actually lives
Rails encrypted credentials are a file per environment:
config/credentials/production.yml.enc, decrypted byconfig/credentials/production.key, which is the thing you never commit. The workflow Rails intends isbin/rails credentials:edit --environment production, an editor opens, you type YAML, it re-encrypts on save.Rails credentials per environment is the whole design, and that workflow is fine. It is also why people give up on the first evening. You have to know the file exists, know the flag, know which keys the application reads, and get the nesting right with no feedback until something boots wrong. A wizard that lists the keys, shows which are missing and writes the file is the same operation with the guesswork removed.
The important part is that the destination does not change. The wizard is a nicer editor for
credentials:edit, not a second configuration system. Nothing moves to the database, so nothing about how the application reads its secrets is different because a wizard exists.Gated to local, every action
One
before_action, applied to the whole controller rather than to the write actions, and that breadth is the point. A deployed wizard would be readable as well as writable, and a read of that screen is a list of exactly which secrets this installation holds.Three things follow from the gate. A deployed console cannot rewrite a secret at runtime, so the credentials in a running production app are whatever was committed and deployed, full stop. The live checks, which call Stripe and send mail with a key somebody just typed into a browser, cannot be triggered from the internet. And the wizard's navigation link is hidden rather than shown and refused, so the deployed console does not advertise a door it will not open.
The read-only filesystem
Beyond the security argument there is one that would break the feature anyway. A production dyno on Heroku, and a container almost anywhere else, has an ephemeral and effectively read-only filesystem. Writing
production.yml.encthere would either fail outright or succeed into a filesystem that disappears on the next restart, taking the secrets with it.Worse than both: a partial write. The encrypted file is rewritten whole, so a failure midway leaves an unparseable file, and the next boot cannot read any credential rather than missing one. The local gate removes the entire class of problem by never attempting the write anywhere it cannot complete.
What a blank field has to mean
The form shows existing secrets masked, because showing them in full would put every key in the browser history and in any screenshot the founder takes. Masked values create an ambiguity that has to be resolved deliberately: when the field displays dots and the founder saves the step without touching it, the submitted value is either the dots or nothing at all.
The rule is that a blank submit leaves the stored value alone. The alternative, treating blank as an instruction to clear, means saving one step of the wizard silently empties every other key on the page, and the founder finds out at the next boot with an application that cannot reach Stripe.
This is not a nicety. It is the difference between a wizard you can revisit to change one thing and a wizard you can only ever fill in once, correctly, in a single pass.
Testing a key without lying about it
The wizard offers a live check: paste the Stripe secret key, press the button, get a yes or a no. The naive implementation calls
Stripe::Account.retrieveand reports the result, and it is misleading in exactly one case.Stripe.api_keywas read once at boot. When you are configuring the environment the server is currently running as, and the key in the box differs from the one in memory, a green tick would confirm a key the application is not using. The guard asks for a restart instead.For any other target, configuring production from your laptop, the running key is irrelevant and the submitted key is tested directly. Same button, two meanings, and only one of them is a claim about the running process.
What the wizard does not do
The wizard does not manage secrets over time. There is no rotation, no history, no record of who changed what, because there is one founder and a git history of the encrypted file.
It also does not remove the deploy. Writing
production.yml.enclocally is step one of three: the file has to be committed and the application has to be deployed before production sees anything. A wizard that made a change feel live when it was not would be worse than no wizard, so the distinction is worth keeping in front of whoever is using it.