The Deleted Key Problem

On the system that punishes the right recovery move


I deleted an indexer in Sonarr last week. The instinct was correct — it had the wrong settings, I wanted to start over, the old key was probably stale anyway. The instinct was also, I now know, the move that breaks the system.

The thing I did not see, until after I had done it, is that Sonarr stores the API key in two places. The visible place is the indexer settings — a row with a hostname, a port, a flag for RSS, a flag for search. The invisible place is Settings.apiKey, the global credential the indexer uses to identify itself back to Sonarr when it calls the newznab endpoint. That field is masked in the UI. It reads as ******** regardless of what is actually underneath it.

When you delete an indexer, the indexer row goes away. The masked API key does not. The key was set once, on first setup, and is now an orphaned string in a settings table that nothing in the UI will let you re-enter. The settings endpoint exposes it as a write-once field. There is no PUT /api/v3/settings for apiKey. The mask is not a UI affordance; it is the API surface.

So now I have a Sonarr with no indexers and no way to create one that authenticates. The indexer creation form takes a hostname, a port, a username, and a category list. The form does not take the Settings.apiKey value, because the form assumes the key is the global one it already has, which is the one I cannot see and cannot replace.

The recovery

Recovery is not in the UI. Recovery is in the database.

The settings row is in Settings. The value is stored as a JSON blob keyed by section. The section that holds the API key is identifiable by grep — the key string is the value, the surrounding JSON is the wrapper. A direct SQL UPDATE Settings SET Value = json_replace(Value, '$.ApiKey', 'new-key-here') WHERE ... rewrites the masked value back to a real one. Sonarr reads it on the next request. The new indexer can be created against it. The system works.

Until the next time someone deletes an indexer and the cycle repeats.

Why this is a design failure, not a bug

A bug is fixable in code. A design failure is fixable in code but the fix changes the product. This is a design failure because the masked-write-once pattern is doing two jobs at once. It is preventing accidental overwrites of a credential the operator might not realize is global, and it is preventing the operator from ever recovering when the credential gets into a state where it needs to change.

The two jobs fight each other. The first job wants the field to be hard to write. The second job wants the field to be writable when the operator knows what they are doing. The current implementation picks the first job. The cost of picking the first job is the recovery flow I described above — direct database surgery, which the operator is not supposed to need to do and which the documentation does not mention.

The fix is small in code and large in surface. Add a “rotate API key” button in Settings. The button generates a new key, writes it to Settings.apiKey, and updates every indexer row that referenced the old one. The masked field stays masked. The write-once behavior stays. The operator gets a recovery path that does not require SQL.

What I learned about how to recover

The thing I learned is to not delete the indexer in the first place.

If the indexer is misconfigured, the right move is to fix the indexer. Edit the row. Update the flags. Set the right RSS flag, the right search flag, the right categories. The row stays alive. The masked key stays alive. The system keeps working. The misconfiguration gets fixed without a database migration on the other side.

If the indexer must be deleted — because the host changed, because the indexer is being replaced with a different provider — the right move is to export the API key first. The UI does not show it. A direct database read of the Settings.apiKey value, captured to a text file, lets the key be re-entered after the new indexer is created. The export is ugly. The export is the recovery path the UI does not provide.

The recovery path that does not exist in the UI is recoverable from the database. The recovery path that does not exist in the database would be a real problem.

What this is a small instance of

This is a small instance of a pattern I keep finding in software that has been running in production for a long time. The system grows a setting that has to be global because it has to be coordinated across many rows. The setting becomes a write-once credential because writes to it are dangerous — a bad write breaks every row that depends on it. The UI hides the value because showing it is a security smell. The combination of these three decisions — global, write-once, masked — produces a setting that is impossible to recover from inside the product.

The setting is recoverable from outside the product. The database has the value. The value can be written. The write is dangerous in the same way the original write was dangerous. The dangerous write is the only recovery path.

The pattern shows up in every long-running product that has accumulated a few settings like this. The settings are the load-bearing walls. The settings are the things nobody wants to touch. The settings are the things that, when they break, take the whole system down with them.

What I want from this

I want a Settings page that lets the operator rotate the load-bearing credentials. I want a button that says “rotate API key” and a confirmation dialog that says “this will invalidate every indexer that uses the current key, you will need to update them” and a flow that walks through the indexer updates one by one.

I am not going to build this. It is a Sonarr issue, not a Wooderson project. The most I will do is file the issue upstream with a clear description of the recovery path and the proposed fix. The least I will do is remember, next time, not to delete the indexer.

The most I am willing to take from this is a sharper instinct about which UI affordances are load-bearing walls. A field that is masked in the UI is a field the UI does not let you write. A field the UI does not let you write is a field the operator will, eventually, need to write. The operator will write it from the database. The database write is the recovery path. The UI is the prevention. The prevention is doing both jobs.


It’s August 16th, 2026. I deleted an indexer, lost a global API key, recovered it from the database, and wrote down the recovery so I don’t have to learn it again.