The last thing I encrypted was my own journal
How I encrypted every existing journal in a deploy that could not be rolled back, and why an un-undoable change inverts the whole release process.
The last entry I encrypted today was my own. That was deliberate. The migration I shipped reached into the database and encrypted every journal entry that was already sitting there in plain text, and I arranged the whole day so that my own journal, the one I actually care about, was the very last thing it touched.
The reason I was that careful comes down to one property. Once an entry is encrypted, nothing turns it back.
When there is no undo button
Most deploys come with a quiet safety net. If something looks wrong after you ship, you revert the code and you are roughly back where you started. This deploy did not work that way. Encrypting data in place means the readable version is gone the moment the change runs. Reverting the code afterward does not unscramble anything, because the code was never the thing holding the plaintext. The data was.
So the recovery plan could not be "roll it back." It had to be a copy of the data, made before the change, living somewhere the change could not reach. I built the migration to offer that copy first, before it touched anything, and to recommend it firmly. The key that unlocks an entry is held by the user and tied to their sign-in, not kept by us. That is good for privacy, and it is exactly why a backup matters. If a user loses access to the account that holds their key, we cannot fetch their journal back for them. The backup is the only door left.
The principle I kept returning to: when a change cannot be undone, the recovery has to exist before the change runs, not after. A rollback you design once the incident is already underway is a plan for a different, easier kind of failure.
Test outward-in, and put your own data last
The harder question was how to earn confidence in a migration before running it on real people's journals. I built a ladder, from the safest and least real to the most real.
The first rung was synthetic accounts running against a faithful copy of the production database. That proved the logic. Entries encrypted, nothing left half finished, the right accounts skipped, an interrupted run able to pick up where it stopped. The second rung was a throwaway account on the real infrastructure: the actual sign-in, the actual cloud key storage, the actual browser, going through the actual upgrade screen. That proved something the synthetic tests could not, which is that the whole flow worked end to end on real systems and not just inside a test harness.
The last rung was my own journal. Forty-some real entries, the thing I actually cared about, and I touched it last, on purpose, and only after I had saved a backup of it. By the time my real data was on the line, every part of the path in front of it had already been walked by something I could afford to lose.
The principle: order your tests so the thing you can least afford to break is the last thing you touch. Confidence should flow toward your most precious data, not start there.
The irreversible deploy punishes assumptions
Here is the moment I am least proud of and most glad I caught. Partway through testing on the real infrastructure, I had the upgrade screen open and ready to continue. The screen does not show which account you are signed in as. I assumed it was my throwaway test account, because that was the account I meant to be using.
Before tapping anything, I checked which account was actually signed in by reading the stored session directly. It was my main account. My real journal. One tap would have encrypted my real entries on a preview build, out of sequence, before the real deploy had even happened. My assumption was wrong, and the screen gave me no way to see that it was wrong.
What strikes me is how undramatic it was. No alarm went off. The interface looked exactly the same whether I was one tap from the right thing or the wrong one. That is the quiet danger of an irreversible step. It does not feel different when you are about to make the mistake.
The principle: on a step you cannot undo, verify identity and state from the system itself. Do not infer them from what you expect to be true. The cost of checking is a few seconds. The cost of assuming is the thing you cannot get back.
A backup nobody can read is not a backup
While testing, I noticed the upgrade screen offered a backup and called it "a readable copy of your journal." What it actually downloaded was a machine-format file. A person cannot really sit down and read it. That was two problems wearing one coat. The word "readable" was plainly inaccurate, and underneath it, the file that can restore your data is not the file a person opens to read their own memories.
I made the backup save both at once. A human-readable document a person can actually read, and the machine file that can restore everything, in a single action, so nobody has to understand the distinction or risk choosing the wrong one. Then I made a smaller and more contested call. I folded that fix into this deploy rather than shipping the inaccurate version and patching it next week. The reasoning was narrow but firm. The only way to guarantee that no user ever met the misleading version was to fix it before the door opened. A fast follow leaves a window, and on this particular screen the window is the exact moment someone decides whether to protect their data.
There was a related temptation I turned down. I had a line ready that told users better recovery options were coming. True, and reassuring, and quietly corrosive, because it tells someone the backup in front of them is optional. It is not optional yet. I cut it.
The principle: a message that is accurate and a little scary beats one that is smooth and misleading, especially when the user's data is what is at stake. Reassurance you cannot stand behind is only a nicer way to lose someone's trust later.
Decouple the announcement from the polish
One more decision, smaller but worth keeping. After the migration shipped I wanted to tell users about it, and I also wanted to polish the sign-in experience first: a branded address, a verified app, the trust signals that make a stranger comfortable. It is tempting to bundle all of that into one tidy launch.
The announcement did not need any of it. It needed exactly one piece, a branded sender address, so the email did not land in spam. The rest of the polish mostly helps new users meeting the app for the first time, who were not the audience for this note. And the verification work is slow, the kind of thing that runs for weeks. Holding a time-sensitive heads-up about a change to people's data behind a multi-week trust project would have meant existing users hitting the upgrade with no warning at all.
The principle: separate what a message actually needs from what would be nice to have around it. Otherwise the slow, nice-to-have work quietly takes the urgent work hostage.
Mental Models
Build the recovery before the change, not after. A rollback you design once the incident is underway is a plan for a reversible failure. For the irreversible kind, the only recovery that counts is the copy you made before you started.
Let confidence flow toward your most precious data, never out from it. Test from the safest, least real version of the work toward the most real, and arrange it so the thing you can least afford to lose is the last thing your change touches.
On an irreversible step, read the state, do not trust your memory of it. The interface looks identical whether you are about to do the right thing or the wrong one. A few seconds of checking is cheap. The assumption is not.
Accurate and slightly scary beats smooth and misleading. When someone's data is on the line, a warning they can act on is worth more than a reassurance you have not earned. Comfort you cannot back up is a loan taken against their trust.