You already know what a privacy policy feels like to read. Two thousand words, written by a lawyer for other lawyers, and at the end of it you still don't know whether the site sets a cookie. The honest reason is that the page and the code have nothing to do with each other. Somebody writes the page once, the code changes forty times, and nobody goes back.
I am one developer running a small site, so I get to try the other thing. This post is what that looks like in practice, and I recommend stealing the idea if you ship anything that makes a promise about data.
First, what there actually is to protect
Being specific here is the whole point, so: two forms on this site write to a database, and between them they store an email address, a role you picked from a dropdown, a two-letter country code, the first 256 characters of your browser's user agent, and a timestamp. That is the complete list.
Your IP address is not one of the things stored. Cloudflare hands the
server a header called CF-IPCountry with a two-letter code in it, and that code
is what gets written down. The address itself never reaches a table.
The two free tools store nothing at all, because they never send anything anywhere. That is not a policy, it is an absence of code.
The problem with writing that down
Everything in the paragraph above is true today. It's true because of specific lines in
src/index.js and specific columns in schema.sql, and it stops
being true the moment somebody changes one of them.
And the change that breaks it doesn't look dangerous. It looks like adding an analytics snippet to see which page people land on. It looks like grabbing the IP for rate limiting. It looks like using a nicer font on the tools page. Each one is a five minute job that quietly turns a published sentence into a false statement, and nothing anywhere complains.
So the page gets a test
scripts/check-privacy-claims.mjs reads the server code, the database schema and
every page on the site, and fails if any of four claims stops holding:
- No IP address is stored. It fails if the Worker reads
CF-Connecting-IP,X-Forwarded-FororX-Real-IP, or if a table grows a column shaped like an address. - No cookies, no analytics, no tracking pixel, no browser storage, on any page.
- Only the home page makes a third-party request, and only for fonts.
- Every page still links to the privacy notice and the terms.
It runs in under a second and it has no dependencies, which matters because a check you have to install something to run is a check that stops getting run.
Proving the guard can actually fail
Here is the part I would push back on if I read this post somewhere else. A test that has never failed is not evidence of anything. It might be checking nothing at all, and you wouldn't be able to tell from a green tick.
So before committing it I copied the site to a scratch directory and broke it six ways, one at a time:
caught: worker reads client IP caught: schema grows an ip column caught: a tool page loads a CDN script caught: a webfont added to the survey caught: localStorage on the landing page caught: a page stops linking to /privacy
Six for six. IF YOU WRITE A GUARD LIKE THIS, BREAK IT ON PURPOSE BEFORE YOU TRUST IT. Otherwise the first thing it tells you is that everything is fine, and you have no idea whether that is true or whether your regular expression has a typo in it.
The bit where it makes me admit something
The third claim is the interesting one, because writing the check forced me to be honest about a thing I'd rather have left unmentioned.
The home page of this site loads the IBM Plex typeface from Google Fonts. That's a request to Google, and Google gets your IP address and your user agent out of it, exactly as it would on any other site that loads a font that way.
Every other page makes no third-party request at all. The two tools, the survey, the privacy page and the terms all use typefaces already on your machine, which is why they look plainer than the front page. The split is deliberate: the pages where you type something are the pages that talk to nobody.
I would not have written that paragraph if the check had not made me encode the rule. It's easier to describe a site as making no third-party requests and quietly not count the one it makes. The check does not let you round down.
What this is not
- It is not legal review. Nobody has looked at my privacy page or my terms except me. I am doing best effort and standard practice, which is proportionate to holding four fields about a few dozen people, and I would say so rather than imply otherwise.
- It does not check wording. It checks the four statements that are claims about code. Everything else on the page is prose I could still get wrong.
- It cannot see what a processor does. Cloudflare hosts this site and handles request data as any host does. My script reads my repository, not theirs.
- It is not a substitute for storing less. The reason this is a small check is that there is very little to check. I prefer it that way, and the check gets harder to write the more you keep.
Check it yourself, on any site
You don't have to take my word for any of this, and I'd rather you didn't:
- Open the network tab on a tool page, then reload. If anything goes to a domain that is not mine, my claim is false.
- Load a tool page, disconnect from the internet, and keep using it. It works, because nothing is being sent.
- Read the source. It is served unminified on purpose, and there is no bundler between what I wrote and what you get.
- Run securityheaders.com against this domain, or any domain. It grades what the server actually sends.
- If you want to understand what a Content Security Policy is doing, MDN's page on CSP is the clearest explanation I have found.
The last one matters more than it sounds. My CSP is script-src 'self', which
means the browser will refuse to load a script from anywhere except this domain even if I
make a mistake and add one. That is a guarantee the browser enforces rather than one I am
asking you to believe.
The page all this is protecting is at benchnotes.io/privacy, and it is short enough to read in two minutes. If you find a sentence on it that the code does not support, that is a bug and I want it: hello@benchnotes.io. If you go and write the same kind of check for your own project, tell me what it caught, because I would bet on it catching something.