vanish
end-to-end encrypted

How Vanish keeps your notes private

Vanish never sees your notes. Your browser encrypts every message before it leaves this tab, and the decryption key lives only in the link you share. Below is exactly what happens — and exactly how to verify it yourself.

What happens when you create a note

  1. You type. Nothing is sent anywhere. There is no autosave, no keystroke telemetry, no analytics script running on this page. Browser spellcheck, autofill, and password managers are explicitly disabled on the message field.
  2. Your browser generates a fresh 256-bit URL key. Using crypto.getRandomValues. The randomness comes from your operating system's secure RNG.
  3. If you set a password, your browser derives a second key from it. PBKDF2-SHA256 with 600,000 iterations over a random 128-bit salt. The password is never transmitted. The two keys are combined via HKDF-SHA256 into the final encryption key. Both are then required to decrypt — link and password. If you skip the password, only the URL key is used.
  4. Your browser encrypts the message locally. Using AES-GCM — an authenticated cipher, so any attempt to modify the ciphertext is detected on decrypt. A new 96-bit initialization vector is generated for each note and never reused.
  5. Only the ciphertext is sent to our server. The HTTP request body is { ciphertext, iv, ttl } (plus salt if you used a password). Neither key is in the body, the URL, or any header. Both are still in memory, in this browser tab.
  6. The URL key is placed after the # in the link. The URL fragment — the part after # — is not transmitted by browsers to servers(by specification). It lives only in your URL bar and the recipient's. The password, if you set one, lives only in your head.
  7. The server stores the ciphertext with a TTL. Upstash Redis deletes the ciphertext atomically when the TTL elapses. No cleanup job, no chance of a stale blob lingering — the encrypted blob disappears from the database.
  8. The recipient opens the link. Their browser reads the URL key from the # fragment, fetches the ciphertext by ID, prompts for the password if one is required, re-derives the key, and decrypts in their tab. Our server never learns who opened it.

What we can and cannot see

Ciphertext (useless without the key)yesIV (random, not secret)yesSalt (random, not secret) — only when a password is usedyesCiphertext size and time of creationyesYour TTL choiceyesWhether a password is required (presence of the salt field)yesYour IP address at the hosting edge (our Cloudflare Worker strips identifying headers before anything reaches our application server)noYour plaintext messagenoYour URL keynoYour passwordnoWho opens the link, or whennoYour identity (we never ask)no

Verify it yourself

Open your browser's developer tools (Cmd+Option+I on macOS, Ctrl+Shift+I elsewhere) and switch to the Network tab before you create a note.

  • Filter by Fetch/XHR and watch the POST /api/create. The request body is strictly { ciphertext, iv, ttl }, plus salt if you set a password. Your plaintext is not in it, anywhere. Your password is not in it, anywhere.
  • The response is { id, expiresAt }. The server returns an ID and a timestamp. Nothing else.
  • When the recipient opens the link, the GET /api/get/:id response contains only { ciphertext, iv, expiresAt } (plus salt for password-protected notes — needed by the recipient's browser to re-derive the key). The server still doesn't have either key — decryption happens client-side.

The full source is public at github.com/twobitapps/vanish. The files you want to read:

  • lib/crypto.ts — AES-GCM, PBKDF2, HKDF helpers
  • app/page.tsx — the create flow (the only place plaintext exists on the send side)
  • app/m/[id]/page.tsx — the view flow (the only place plaintext exists on the receive side)
  • edge/worker.js — the Cloudflare Worker that strips identifying headers before requests reach our origin

Nothing else touches the plaintext or the password.

Optional password layer

When creating a note you can require a password in addition to the link. Two factors, both necessary to decrypt:

  • The URL key (in the # fragment, 256 random bits).
  • A password you choose, run through PBKDF2-SHA256 at 600,000 iterations in your browser. The random 128-bit salt is stored alongside the ciphertext so the recipient can re-derive the key; the password itself is never transmitted.

The two outputs are combined via HKDF-SHA256 into the actual AES-GCM encryption key. Even if the URL leaks (history sync, clipboard, screenshot), a note without the password stays sealed. Share the password out-of-band — a phone call, a Signal message, anything that doesn't travel alongside the link.

Strength note: a good password is worth more than a long one. A memorable 4-word diceware phrase beats a complicated short string. Avoid anything you've reused from elsewhere.

Auto-vanish, on both sides

The server side is simple: Upstash Redis holds the ciphertext with a server-set TTL. The moment the TTL elapses, the blob is dropped — no cleanup cron, no stale writes.

The client side is where most “expiring note” tools drop the ball. Vanish doesn't. When the countdown hits zero in an open tab, the view page flips to a vanishedstate and drops the plaintext reference from React state in the same tick, so the string becomes unreachable from any running code. We disable the browser's back-forward cache on view pages (Cache-Control: no-store), so navigating back cannot resurrect the note from bfcache memory. And a visibilitychange + focus listener re-checks expiry the moment a backgrounded tab returns to the foreground, so a tab left open for hours cannot display a stale note after its TTL has passed.

A log-stripping edge proxy

Requests to hy.gl do not reach the application server directly. They arrive at a Cloudflare Worker whose only job is to strip every identifying header from the request before forwarding to our Vercel origin:

  • IP markers: X-Forwarded-For, CF-Connecting-IP, X-Real-IP, True-Client-IP, Forwarded
  • Geo signals: CF-IPCountry, CF-IPCity, CF-IPContinent, CF-IPLatitude/Longitude, CF-Region, CF-Timezone
  • Fingerprints: User-Agent, Accept-Language, Referer, every Sec-CH-UA-* client hint, Device-Memory, Viewport-Width

By the time a request reaches our application logs, the IP is 0.0.0.0 and the user-agent is the constant vanish-edge. No geo, no language, no browser fingerprint — nothing distinguishing one visitor from another beyond request timing and size.

Cloudflare itself still sees real client IPs for a short retention window at the edge. That is the remaining single-vendor trust. If you need anonymity stronger than that, reach the site through Tor or a VPN. The Worker's source is public at edge/worker.js — every header in the strip list is readable in one file.

URL shortener, same principle

When you shorten a URL, your browser splits the URL at the first # and sends only the part before the #. If you shorten a Vanish link like hy.gl/m/abc#KEY, the server stores hy.gl/m/abc; the key stays in your browser. When someone visits the short link, their browser re-attaches the fragment automatically during the redirect — a standard browser behaviour that preserves zero-knowledge for shortened notes.

The honest caveats

  • If the link is lost, the note is unrecoverable.The operator cannot recover it; we don't have the key. This is by design.
  • If you set a password and forget it, the note is unrecoverable.We can't reset it, bypass it, or brute-force it for you — we never saw it.
  • Anyone with the link can read it until it expires (or, if you added a password, anyone with both the link and the password).
  • The safety depends on the JavaScript this site serves. If the hosting provider or the site operator is compromised and serves modified code, the encryption guarantee could be subverted silently. This is the inherent limitation of browser-based crypto. For a hard threat model, use a native tool with signed binaries.
  • Your browser history keeps the link. The URL (including the key after #) stays in history, in bookmarks, and in the clipboard when you copy it. Use private browsing for sensitive notes.
  • Browser extensions can read the page.Extensions that have permission to read or modify pages can see the textarea. Disable extensions you don't trust before pasting a secret.
  • Cloudflare (our edge) still sees real IPs briefly. See A log-stripping edge proxyabove for what we do about it and what we can't. For stronger anonymity, reach the site via Tor or a VPN.

Cryptographic details

  • Cipher: AES-256 in GCM mode (authenticated encryption). 96-bit IV, 128-bit auth tag. Browser implementation (SubtleCrypto).
  • URL key: 256 bits from crypto.getRandomValues, fresh per note. Encoded as base64url (~43 chars) and placed in the URL fragment.
  • Password derivation (optional): PBKDF2-SHA256, 600,000 iterations, 128-bit random salt. The derived bits are combined with the URL key via HKDF-SHA256 (info = "vanish-v1-password") to produce the AES-GCM key.
  • Paste ID: 96 bits of random from crypto.getRandomValues. Unguessable.
  • Storage: Upstash Redis with server-set TTL and NX (so the same ID can never collide).
  • Transport: HTTPS with HSTS preload header. Server refuses to run without TLS.