Security model, encryption details, and the full flow — for those who want to know.
Text never leaves your browser in plain form. Nothing is sent to the server at this point.
A random 256-bit key and a random 96-bit IV are generated locally. AES-256-GCM encryption runs entirely in your browser using the native Web Crypto API — no libraries, no third parties.
The server receives an encrypted blob it cannot read. The encryption key stays in your browser and is embedded in the link's #fragment — the part after # that browsers never include in HTTP requests.
The server returns a random ID. Your browser assembles the full link: /view/<id>#<key>. Share only this complete link.
Their browser extracts the key from the #fragment, fetches the ciphertext by ID, and decrypts locally. The server deletes the record before responding — the secret is gone from storage the instant it is retrieved.
Any subsequent request for the same ID returns 404. Even the server operator cannot retrieve a secret that has been viewed. Unviewed secrets expire automatically after 1 hour, 24 hours, or 7 days — whichever you chose.
Here is what a generated link looks like and why each part matters:
crypto.getRandomValues()
SubtleCrypto)
Browser-native, zero dependencies, FIPS 140-2 compliant in most implementations
crypto.subtle on plain HTTP — by design
The server is a minimal Node.js / Express application. It stores encrypted blobs in a SQLite database — a single file on disk. This means secrets survive server restarts, unlike an in-memory store.
What the database row contains: a random UUID, the encrypted blob (meaningless without the key), and an expiry timestamp. No IP addresses, no user agents, no plaintext, no keys — ever.
Expired secrets are purged on every server startup and once per hour thereafter. The delete-before-respond design means the record is gone before the HTTP response is sent — even a simultaneous second request will get a 404.
No. The server only ever stores the encrypted ciphertext. The decryption key exists only in the URL fragment, which browsers never include in HTTP requests and which is not logged anywhere on the server. Without the key, the ciphertext is indistinguishable from random noise.
If an attacker intercepts the full link (including the #fragment) before the intended recipient opens it, they could view it first. This is the fundamental trade-off of any link-based secret sharing system. Use a trusted channel to share the link — and tell the recipient to let you know once they've opened it so you can detect if it was already consumed.
The secret is deleted the moment it is first fetched. A page refresh will return "Secret not found or already viewed". This is by design — the view page also removes the key from the browser's URL bar immediately after decrypting, so it is not left in your history.
The database row is deleted with a SQL DELETE before the response is sent. SQLite writes are synchronous (using better-sqlite3), so the delete is committed to disk before the payload is returned. There is no soft-delete, archive, or log of the content.
GCM (Galois/Counter Mode) provides authenticated encryption — it simultaneously encrypts and produces an authentication tag. Any modification to the ciphertext, IV, or associated data causes decryption to fail with a hard error. This protects against both passive eavesdropping and active tampering. It is the current NIST recommendation for symmetric authenticated encryption and is natively supported in every modern browser via the Web Crypto API.
No. Encryption and decryption run entirely in the browser using JavaScript. This is intentional — it is what keeps the key off the server. A no-JS fallback would require the server to handle keys, which would defeat the zero-knowledge design.