Request flow
What happens when you send one
Two HTTP requests separate a note on your screen from a note on somebody else's. This page follows both of them, names the limits you will hit, and describes what goes wrong when it goes wrong. It covers notes; file shares take a different route, summarised at the end. For the cryptographic parameters, see the security page.
Request one: creating the share
Everything up to this point happens on your machine. You type a note, the browser generates a random 128-bit secret, derives an AES-256-GCM key from it and encrypts. Only then does anything leave the tab:
POST /api/notes
{ "blob": { "v": 1, "alg": "A256GCM", "ciphertext": "...", "iv": "...",
"hasPassword": false, "wrappedKey": null, "kdf": null },
"ttlSeconds": 86400 }
200 OK
{ "id": "Ku3nV8pQ2xLd" } The server validates the envelope, stores it under a freshly generated 12-character identifier and sets an expiry. It has no way to inspect what it just stored. The response is the identifier and nothing else.
Your browser then assembles the link locally by joining the identifier to the secret: https://classified.mainly.art/n/Ku3nV8pQ2xLd#<secret>. The part after the # was never in either the request or the response.
Request two: opening it
GET /api/notes/Ku3nV8pQ2xLd
200 OK
{ "blob": { "v": 1, "alg": "A256GCM", "ciphertext": "...", "iv": "...",
"hasPassword": false, "wrappedKey": null, "kdf": null } } The worker fetches the record, deletes it, and returns the ciphertext. Decryption happens in the recipient's browser using the secret from the fragment. A second request for the same identifier gets 404.
Password-protected shares add one step on the recipient's side rather than one request: the stored envelope carries a wrapped key plus its salt and iteration count, and the browser has to unwrap it with the password before it can decrypt anything. The server is not involved in checking the password and could not verify it if asked.
Limits
These are enforced server-side. Exceeding them returns an error rather than truncating.
| Limit | Value | Notes |
|---|---|---|
| Minimum TTL | 5 minutes | Short enough for a live handoff over a call. |
| Maximum TTL | 24 hours | A deliberate ceiling. A secret that needs to sit for a week is a secret that wants a password manager, not a link. |
| Note payload | 200,000 bytes | Ciphertext plus encoding overhead, so the practical plaintext limit is a little lower. |
| File size | 10 MB by default | Per file, and set per plan rather than globally. Larger transfers are outside what this tool is for. |
| Crypto envelope | 8,192 bytes | The metadata wrapper is tiny by design; the cap exists to reject malformed submissions early. |
| Identifier | 72 bits | 12 base64url characters, independent of the decryption key. |
How expiry actually behaves
The TTL you choose is passed to the storage layer as an expiration, not tracked by a job we run. That has one consequence worth stating plainly: expiry is prompt but not instantaneous. A share past its TTL stops being retrievable, but the underlying record is reclaimed by the storage layer on its own schedule rather than at the exact second the clock runs out.
In practice this means the TTL is an upper bound on availability, not a precisely timed destruction event. For the threat models this tool is built for, that distinction does not matter. If it matters for yours, it should change your decision.
Deletion on read and expiry on TTL are two independent mechanisms, and both are active on every share. Whichever happens first ends the share. The TTL is what guarantees a link nobody ever opens does not sit around indefinitely.
Failure modes
The link arrives already spent
Link previewers in chat apps, corporate mail security scanners and URL sandboxes all fetch links to inspect them, and any of them can be the first reader. If a recipient opens a link and sees "not found" without having opened it before, an automated client reaching it first is the explanation to rule out before assuming a fault.
Sending the link through a channel that does not unfurl URLs, or using password mode so the fetch alone reveals nothing, both help.
Two people open it at the same moment
Fetch and delete are separate operations against an eventually-consistent store. Requests arriving close together can both succeed before either delete takes effect. One-time delivery is a strong default and not a transactional guarantee. This is covered in more detail under the security model.
The link is intact but the content will not open
Usually a truncated fragment. Chat clients and email quoting sometimes wrap or clip long URLs, and losing characters after the # produces a link that reaches the right record with an incomplete key. The client validates the secret's length and reports a malformed key rather than showing a decryption failure, so this is distinguishable from a wrong password.
Nobody can recover it, including us
There is no reset path. We hold no key, so a lost link or a forgotten password makes a share permanently unreadable, and a consumed share is gone. Treat a one-time link as a delivery mechanism, never as storage.
Files take a different route
File shares do not follow the flow above. Creating one requires a signed-in account with a verified email address, because file storage carries a per-plan quota that has to be attributed to someone. Retrieval is two steps rather than one: the recipient's browser asks for the envelope metadata first, then requests the file itself. The stored copy is deleted at the moment the download endpoint serves it, not once the recipient has finished saving it, so a connection dropped mid-transfer loses the file for good.
The encryption is identical, with the same primitives, parameters and key handling. What differs is the account requirement, the quota and the extra round trip.
Further reading
- RFC 3986, URI Generic Syntax, section 3.5, on why the fragment never reaches the server.
- W3C, Web Cryptography API, the browser interface doing the encryption.
- Cloudflare, How KV works, on consistency and expiration behaviour in the storage layer.