Why the key sits after the #
One line in the URL specification, doing more security work than most of the stack.
The part of a URL after the # is called the fragment, and browsers do not send it to the server. That single behaviour, specified decades ago for jumping to headings, is what makes it possible to hand someone a link that a server can store but cannot read.
What the fragment is
Take a URL apart:
https://example.com/page?q=hello#section-two
Scheme, host — where to connect. Visible to your network and to DNS.
Path (/page) — sent in the request. Lands in server logs.
Query (?q=hello) — also sent in the request. Also lands in logs, and in analytics, and in referrer headers.
Fragment (#section-two) — not sent. The browser keeps it, uses it locally to scroll to an element, and never puts it in the HTTP request.
This is not a convention or a courtesy. It is how the URL specification defines the fragment: it identifies a part of the already-retrieved resource, so there is no reason to send it, and browsers do not.
Using it to hold a key
Because the fragment stays on the client, it can carry something the server must never see. When you write a note here:
- Your browser asks the operating system for 256 bits of cryptographic randomness — crypto.getRandomValues, the same source used for TLS session keys.
- It encrypts your text with that key using AES-256-GCM, via the browser's built-in Web Crypto API. No third-party crypto library is loaded; there is nothing between your text and the browser's own implementation.
- It sends the ciphertext to the server, which stores it and returns an identifier.
- It builds the link: the identifier in the path, the key in the fragment.
When the recipient opens that link, their browser requests /n/<identifier> — the fragment is withheld — receives the ciphertext, reads the key from its own address bar, and decrypts locally. The server participated in the exchange without ever holding both halves.
It converts a promise into a fact. Instead of "we do not read your notes", the statement becomes "we cannot", and the reason is checkable: open your browser's network panel, open any share link, and look at the request. The characters after the # are not in it.
Where the fragment also does not go
Beyond the request itself, the fragment is absent from several places that routinely leak URLs:
Server access logs. They record the request line, which has no fragment. On this site access logging is switched off anyway, but the fragment would not be there even if it were on.
The Referer header. Browsers strip the fragment before sending a referrer. This site additionally sends Referrer-Policy: no-referrer, so the identifier does not leak either.
Intermediaries that terminate TLS. A proxy or CDN in the request path sees the decrypted HTTP request — path, headers, body. It still does not see the fragment, because the browser never put it there.
Where it does go — be aware of these
The fragment is not magic, and pretending otherwise would be the kind of overclaiming this site exists to avoid:
It is in the URL. The whole link, key included, is in your address bar, your browser history, and whatever you paste it into. If you send the link over a channel someone else can read, they have the key. The fragment protects the secret from the server, not from the messenger.
It is visible to anything running in the page. Any script executing on the page can read location.hash. That is why the Content-Security-Policy here is default-src 'none': no analytics, no advertising, no external fonts, no third-party code of any kind. A single foreign script on the page that shows your secret would undo everything above.
It is in browser history. After a note is read, this site rewrites the address to a plain path so the key does not linger visibly, but a shared or backed-up browser profile is still a shared browser profile.
The limit that remains
The encryption code that does all this is delivered by the server on each visit. Whoever controls the server could therefore serve altered code to future visitors — code that quietly posts location.hash somewhere. Subresource Integrity does not remove this, because the same server publishes the hashes.
Two things limit it. Anything already encrypted stays sealed, because changing the code cannot retroactively obtain a key that was never transmitted. And the change is detectable: the hashes are published at /integrity.json, and /verify compares the code you were just served against them and against what your browser recorded on previous visits. An attacker has to be consistent across every file, over time, for every visitor. That is a much harder problem than a quiet one-off swap — but it is not zero, and we say so on /security rather than leaving you to find it.
See it yourself
Write a note on the home page, open your browser's developer tools, switch to the network panel, and open the link. Compare the request URL with what is in your address bar. The difference is the key.
Related: how a one-time secret link works end to end, what burn-after-reading really means, and the full security architecture.