Rollbook went live without a terms of use, a privacy policy, or a cookie policy. That's a normal way for a solo project to start and a bad way for it to stay, so I sat down to write the three pages properly. The interesting part wasn't the writing. It was everything the writing made me look at.
A privacy policy is only honest if it matches the code, and checking that match kept surfacing things that weren't quite right. Not broken, exactly — just not what I'd have said they were if you'd asked me cold. So the pages ended up being the excuse, not the point.
Fonts, and a bug the build never showed me
The first honest sentence I wanted to write was "no third-party scripts." I couldn't, because every page was pulling three font families from Google Fonts on load — a preconnect, a stylesheet request, an IP address handed over before the page even painted. Self-hosting them was the obvious fix, so I pulled in @fontsource packages and added the imports to the stylesheet.
The build went green. Twenty-three @font-face rules compiled in. I almost stopped there.
Checking dist instead of trusting the build log, I found zero font files actually emitted — every one of those rules pointed at a woff2 that didn't exist. Tailwind v4 inlines CSS @imports before Vite's asset pipeline ever sees them, so the font URLs never reached the bundler. In production, every face would have 404'd silently and fallen back to Georgia. A green build and a working build are not the same claim, and this was the reminder. Moving the imports into the JS entry point instead of the stylesheet fixed it — Vite fingerprints and emits the files correctly from there — and the fix is one line, once you know which layer is eating the reference.
The legal basis I'd picked was wrong
Publishing a roll to a public page felt like a consent action, so that's what I wrote: users opt in, they can opt out, consent withdrawn. It reads fine until you think about what consent actually means under GDPR — it's what a data subject gives a controller, not what a user does with their own record. Publishing a roll is closer to a feature the user asked for than something done to them.
The distinction isn't pedantic. Naming it consent creates an obligation: withdrawal has to be as easy as giving it, and it has to actually stop the processing. Unpublishing a roll can't un-cache a page that search engines already indexed, which means the consent framing was writing a promise the product couldn't keep. Recasting it as contract — the user asked for a public page, here it is, here's how to take it down — matches what the feature actually does instead of what sounded more careful on first pass.
Getting this right meant going back to first principles for the other clauses too: naming the actual Supabase region so the EEA-transfer paragraph wasn't just hedging, stating retention in terms of criteria instead of "a while," and fixing an unenforceable "we can change these terms and your continued use means you agree" clause that the EU's unfair-terms directive would have thrown out anyway.
Building the delete button instead of writing around it
The gap I kept writing softer and softer language around was account deletion. There was a "request deletion" flow — type DELETE, it opens an email to me, I delete the account by hand within a few days. Every draft of the privacy policy wanted to describe that honestly, and every honest description read worse than just fixing it.
So I built the actual thing: a database function that deletes a user's rolls, presets, and auth identity in one transaction, callable directly from the client. The whole safety property is that it takes no arguments — it always acts on whichever account is currently authenticated, so there's no ID field to tamper with and no way to name someone else's account by mistake.
The interface borrows from a pattern GitHub uses for repository deletion: you don't click "yes" on a modal, you type the name of the thing you're about to destroy. For Rollbook that's the account's own email address, not a fixed word — DELETE can be typed from muscle memory, an email address forces you to actually look at what's in the box. The warning states the real cost first — how many rolls, that public pages stop resolving, that printed labels become dead links — with the button greyed out until the typed text matches exactly. Get the RPC call wrong and the panel stays open with the error on screen; a dismissed dialog on a failed delete would read as success, and that's the one lie this feature can't tell.
It got its own color, too. The button had been reusing the app's ordinary link blue, which reads as "click me," not "this is permanent." Danger zones get amber now, across every theme the app ships.
Why bother
None of this started as a redesign. It started as three pages of legalese that needed to be true. But a policy that describes your product accurately is also an audit of your product, and mine turned up a silent font bug, a legal fiction I'd talked myself into, and a delete button I'd been avoiding by writing around it instead of building it. The paperwork didn't cause any of that. It just refused to let me keep not looking.