Michael Mines / war stories

Safari would not remember the key

Happened July 22, 2026

The book is locked. You type two keys once, the page derives a key from them, decrypts itself, and remembers you. That is the whole product promise: a family opens their book, not a login form.

Then a reader on an iPhone told me they had to type the keys every single time.

The symptom pointed at the wrong place

Storage eviction was the obvious suspect. Safari is aggressive about clearing site data, and a family might open their book once a month, which is exactly the pattern that gets swept. I could have shipped a longer expiry, called it fixed, and been wrong.

Reproducing it took one device. On desktop the key persisted. On iOS Safari it never persisted at all, not even for the next thirty seconds. That is not eviction. Eviction takes time. This was failing immediately.

The bug was in the error handling, not the crypto

The unlock path derived a key, then called exportKey to get raw bits it could store. WebKit refuses exportKey on a non-extractable key in a way the other engines do not. The call threw.

And the throw was caught. It landed in a general catch around the unlock flow whose job was to show "those keys did not work" for a wrong password. So the decrypt had already succeeded, the book was already on screen, and the persistence step failed silently into an error path built for a completely different failure. The reader saw a working book and no error. The next visit, nothing had been saved.

A swallowed error in a path that succeeds visibly is the hardest kind to see. Nothing is red. Nothing is in the console. The feature just quietly does not exist on one engine.

The fix, and the part after the fix

deriveBits instead of exportKey, so the raw material never has to be exported from a key object at all. Storage in two places, localStorage and a ten year cookie, because the two get evicted under different rules. A re-save on every successful open, so the eviction clock resets each time a family reads.

Then the harder half. Books already delivered carried the old code inside them. Every one of those readers was on the broken path, and the books are encrypted artifacts sitting in private storage, not pages I can redeploy.

The migration patched the page shell in place without ever touching the encrypted payload. Same ciphertext, same salt, new unlock code around it, applied through a temporary keyed route that existed for exactly as long as the migration took. When it was done I deleted the route and confirmed it returned 404, because a migration endpoint left running is just an unauthenticated write endpoint with good intentions.

What is left, and why it is in the FAQ

Safari can still evict storage under real pressure. No code fixes that. So the FAQ says so plainly: if your device clears site data, you will need the keys again, keep them somewhere safe. Writing the residual failure down is not an admission that the fix is incomplete. It is the difference between a product that has a known limit and a product that surprises someone in five years.

The fix is public. Open the demo book at ourfamilyheirloom.com/demo/index.html with the keys sofia and mateo, on an iPhone if you have one, and close the tab. It will remember you.

The lesson I actually took: when a feature works on one engine and not another, suspect the error handling before the API. A catch block written for one failure will happily swallow a different one, and it will do it without a single red line anywhere.