Every team shares secrets, a database password, an API key, a login for a contractor. Almost nobody shares them safely. locc.it is the tool I built so I wouldn't have to: paste a secret, get a link and a passcode, and know the secret was encrypted before it left the browser, can be opened exactly once, and will expire whether or not anyone opens it.
The problem
Watch how secrets actually move between people and it's grim. A password gets pasted into a Slack DM. An API key goes out by email. A production token lands in an iMessage thread because it was the fastest window open. The secret reaches the right person, and then it just stays there.
Every one of those channels keeps a copy. Slack retains message history and syncs it to every device on the account. Email lives in two mailboxes and two sets of backups. Nothing expires. A credential shared once in 2019 is still sitting in a searchable archive today, long after anyone remembers it's there. The leak isn't the moment of sharing, it's the indefinite retention afterward.
I wanted a tool with three properties: encryption in the browser (a secret is ciphertext before it leaves the sender's machine), one-time access (a link opens once, then the secret is gone), and a required expiry (every secret has a deadline, there is no option to create one that lasts forever).
The architecture
locc.it is a React frontend and an Express + MongoDB backend. The shape of the security model matters more than the stack: the browser handles both encryption and decryption, the server only ever holds opaque ciphertext, and nothing sensitive is persisted on the server, ever.
Encryption in the browser
When a sender submits a secret, the browser generates a random passcode and runs CryptoJS AES over the payload before anything is transmitted. Only the ciphertext (and the URL identifier, and the chosen expiry) are sent to the server. The passcode never touches the server at write time; the sender shares it with the recipient out-of-band, alongside the link.
What the server actually does
The server's standing ability to read anything is zero. There is no master
key, no per-user key, no recovery flow. The ciphertext sits in MongoDB
alongside the URL identifier and the expiry, and that's it. When the
recipient hits the link, the server returns the ciphertext, marks the
record as viewed (starting a short grace window for refreshes), and never
sees what happens next. The recipient's browser runs the AES decrypt
locally; the passcode never leaves their device. Once the plaintext is on
screen the client calls DELETE /api/secret/:url and the
record is purged. The passcode is never logged, never transmitted, never
persisted. The plaintext only ever exists on the two browsers at the ends
of the exchange.
A 60-second grace window after the first fetch tolerates page refreshes during the decrypt UX. After that the record self-cleans on the next access attempt, with a cron sweep as belt-and-braces backup. Either way the link works exactly once.
One-time access, mandatory expiry
A locc.it link works exactly once. The first successful open returns the secret and destroys it; a refresh or a second click finds nothing. Every secret also carries an expiry, and it isn't optional: a link that's created and never opened doesn't linger, because once its deadline passes a cleanup job purges the record whether it was viewed or not. Between single use and required expiry, every secret has a short, guaranteed lifetime instead of an open-ended one. A locc.it link is only ever live, used, or expired.
Brute-force protection and bot defence
Brute-force resistance is structural rather than tracked. An attacker would need both the URL and the passcode, and the URL is one-shot: a single fetch starts a 60-second grace window after which the record is purged regardless of whether anyone managed to decrypt it. That leaves an attacker with at most a minute of offline attempts against a ciphertext they can't re-acquire. The decrypt and encrypt routes are individually rate-limited (5 and 10 requests per minute respectively) to slow scraping. Forms run behind Google reCAPTCHA, and a cron job sweeps expired records on a schedule. None of these are exotic, but together they close the obvious back doors around the cryptography rather than relying on the encryption to do all the work.
Accounts are optional
A first-time sender can use the tool without signing up. OAuth via Google, GitHub, or Facebook is there for people who want a small dashboard of secrets they've sent, but it's never required to share or receive one. The default path is anonymous on purpose: the higher the bar to send a secret safely, the more people just paste it into Slack.
Key decisions
Encrypt on the client, store ciphertext only
The simplest way to build a secret-sharing tool is to encrypt on the server with a key the server holds. It's easy to build and it means a database dump is a secrets dump. Doing the encryption in the browser, with a passcode that's never written down on the server, makes the stored data inert by itself. A copy of the database alone is just a pile of locked boxes; the keys aren't anywhere on disk.
Expiry is mandatory, not a setting
Most sharing tools make expiry optional and default it generously. locc.it makes it required and bounded. The cost is that a sender can't create a permanent link, even if they want one. The gain is that there is no such thing as a forgotten locc.it secret: the failure mode where a credential quietly outlives its purpose can't happen, because the default behaviour is the safe behaviour and there's no way to opt out of it.
A web app, not a developer tool
locc.it could have been a CLI or a library, cleaner to build, and a natural fit for the engineers most fluent in why it matters. I made it a plain web page instead: paste a secret, get a link. The people who most need to share credentials safely aren't all developers, and a tool only improves security if the least technical person in the exchange will actually use it.
Outcomes
locc.it has been live since 2019 and still runs today, years of quiet uptime for something that has never asked for much attention. It does one thing: move a secret from one person to another without leaving a copy behind. It's still the link I send whenever someone needs a credential from me, and building it was the cleanest way I know to think hard about a real threat model end to end: what the server holds, what survives a view, and for how long.
What I'd do differently
I'd document the security model openly. Publish exactly how a secret is protected end-to-end, so the model is something a visitor can verify rather than take on faith. For a tool whose entire pitch is trust, "it's encrypted" should be a claim anyone can check, not one they have to believe. The architecture is there to support that audit; the write-up just isn't.
I'd also harden the recipient UX around lost passcodes. Today a wrong passcode shows a generic "incorrect passcode" message and the recipient keeps trying. With a stronger entropy passcode (longer, more diverse character set) and a small client-side hash check against a known prefix, typos could be distinguished from "the wrong link", which would help the recipient ask the sender for the right thing instead of burning their own attempt window.
Six years in, the surrounding app has accumulated features around the edges of its one real job. A tool this focused is usually strongest stripped back to exactly what it does, and deciding what still earns its place is overdue.