"Sync your data to your own Google Drive" sounds like a checkbox. It was not a checkbox. It was five problems in a trench coat, and the last one would have let anyone unlock the paid tier for free.
The premise is nice and clean: I don't want to run a server or hold anyone's data, so I let the extension back up to the user's own Google Drive. Their data, their account, my servers never touch it. Great story. Then I actually used it, and every layer had something wrong.
Problem one was the button. I'd made a single "Sync now" that figured out on its own whether to upload or download based on timestamps. Clever, and completely confusing. As the user, I clicked it and had no idea what just happened — did it push my local data up, or pull the cloud copy down and overwrite what I had? Worse, if the timestamp logic ever guessed wrong, it could silently overwrite good local data with an old cloud copy. I split it into two honest buttons: "Back up to Drive" (upload, never touches local) and "Restore from Drive" (download). You should never have to guess which direction your data is about to move.
Problem two: I enabled the toggle, it hung, and both buttons went grey and stayed grey. I couldn't even turn it back off. The cause was embarrassing and common — the enable path set a "syncing" flag to disable the buttons, then awaited the network call, then cleared the flag. But it had no try/finally. So when the auth popup got closed and the call threw, the "clear the flag" line never ran. The buttons were bricked until reload. Any state you set before an await needs a finally to unset it. No exceptions.
Problem three showed up once auto-backup actually worked: it worked too well. Every data change triggered a backup. But a backup, on success, writes a "last synced at" timestamp to local storage — which is a data change — which triggered another backup. An infinite loop, quietly hammering Drive. The fix was to compare a fingerprint of the real content and ignore the sync-metadata fields, so writing the timestamp doesn't count as a change worth backing up.
Problem four was subtler and I only caught it by asking "what happens if the two sides disagree?" If local has A, B, C and the cloud has B, C, D, what should Restore do? I'd been overengineering a merge with confirmation dialogs and safety copies. Then I stopped and made it dumb on purpose: Restore only works when local is empty — a fresh browser, a new machine, a cleared cache. If you already have data here, the button is disabled. No merge, no "are you sure," no way to accidentally nuke your local work. Simple and impossible to misuse beats clever and fragile.
Problem five is the one that matters. I opened the file that got uploaded to Drive to check it, and there in the JSON was my license object: tier "pro". I'd been syncing the whole app state, license included. Which means: buy Pro on one machine, back up, then hit "Restore" on any other browser — even one that never paid — and you inherit tier: pro. One purchase, unlimited devices, for free. My own backup feature was a license bypass. The fix is a one-liner in spirit: strip the license before upload, and on restore keep whatever license the local machine already has. Entitlement gets decided by each device checking with the license server, never by whatever rode along in a synced file.
None of these were hard once I saw them. That's the whole point of the post. "Back up to Drive" reads like a settings toggle, and inside it were: a UX direction trap, a stuck-state bug, an infinite loop, an over-engineered merge I had to talk myself out of, and a straight-up revenue hole. I found all five not by thinking harder, but by actually using the thing and, once, by just opening the file it produced and reading it.
If you sync app state anywhere, go open the payload right now and read every field. Ask of each one: should this really travel to another device? I'm very glad I asked before a single real user did.
What "simple" feature turned out to be a trench coat full of problems for you?
— building NotebookBloom in public, #10
Top comments (0)