01The key is a ref
I keep the derived AES key outside the reducer that owns the database. In React state it could be serialised into a devtools snapshot, or written to localStorage alongside everything else. I mark it non-extractable at derivation, so JavaScript cannot read the bytes back out either, and locking the vault is one assignment: drop the reference.
02Cleared mid-render
When the vault locks while a secret is on screen, the field clears it with a render-phase state adjustment rather than an effect. React throws that render away and re-runs it, so the plaintext is gone before anything commits. I discard a decrypt still in flight when the lock fires rather than let it land in state.
03Cross-tab sync
Two tabs stay in step through the storage event, but saving the incoming state fires that event back and the tabs bounce the same value forever. A ref marks state that arrived from another tab so the persistence effect skips exactly one write. I re-run the payload through the same migration the initial load uses, because another script on the origin could have written anything into that key.
04Locked, not hidden
A locked vault still renders the whole app. Which service powers which project, what it costs and who owns it are not secret, and I store them unencrypted regardless, so hiding them behind the password would buy no real protection while blocking the main use case. I withhold only the secret values, and I wrote the reasoning into the code at the gate.
05Two dependencies
The app ships on React and React DOM and nothing else. No router, no component library, no CSS framework, no chart library: I drew the sixty icons on a shared 24x24 grid and built the spend donut from stroke-dasharray circles. A day of work, and the build compiles clean at 408 kB, 118 kB gzipped.