Posted 7 mins read
tl;dr: I built SealDrop because every encrypted file-sharing tool solves the sending half of the problem and ignores the receiving half. It's a browser-encrypted send and receive service with no accounts, built in about two weeks, now being tested as a paid product for accountants who need a permanent client upload link.

SealDrop, a sealed file disappearing after it's opened

Most encrypted file-sharing tools solve the wrong half of the problem. They make it easy for me to send you a file. None of them make it easy for you to send me one. Not without an account, not without trusting whatever server sits in between. SealDrop exists to fix that second half. The send side had to exist too, but the receive side is the part I actually built it for.

The problem nobody's tool solved

The pattern repeats once you notice it. A client needs to send documents. A candidate needs to send a signed contract back. Someone needs to drop a log file so you can debug their problem. The default tools are email, which has size limits and leaves plaintext sitting in two inboxes indefinitely, or a WeTransfer-style link, which works fine when you're sending but gives the other person no way to start a transfer without your email address or an account they didn't ask to create.

I used Firefox Send for this for a while. Client-side AES-GCM, key in the URL fragment, gone after download. Mozilla shut it down in 2020 after it got used for malware delivery, and nothing since fully replaced it. The closest alternatives were all built around sending. None treated "I need someone else to send me a file" as a first-class flow.

Why I built it anyway

The receive direction isn't actually hard, it's just a feature nobody had built as the main product instead of an afterthought. Generate an asymmetric keypair in the owner's browser, publish the public half, keep the private half only in a URL fragment HTTP never sends to a server. Anyone can encrypt a file to that public key and upload the ciphertext. Only the owner's browser can unwrap it. Standard ECDH, standard AES-GCM. Web Crypto handles all of it natively.

That's what tipped it from "this is annoying" to "I'm building this." A bounded problem, primitives I already understood, and a scope small enough to say no to almost everything else. No accounts, no folders, no previews, no payments, no third-party crypto libraries. The tagline I held myself to was blunt on purpose, "send or receive a sealed file, no account, gone after use," and most product decisions after that were just checking a feature against it.

Building the first version

I wrote my own product brief before touching code. Two flows, an explicit non-goals list, and a hard rule that the main UI couldn't use words like "AES-GCM" or "asymmetric." That language belongs in a security doc, not in front of someone trying to send a contract.

The architecture choice that paid off constantly was making the local dev stack mirror production one-to-one. Fastify, MinIO, and Postgres locally; Cloudflare Workers, R2, and D1 in production. Same route logic, swappable storage and database adapters underneath. I could test an entire flow in Docker without touching a Cloudflare deploy.

I also moved fast. The MVP plus a security hardening pass happened inside about two weeks, which only makes sense as a solo build because I leaned hard on Claude Code for plumbing and test scaffolding. Every decision that actually mattered for security, what gets logged, what the threat model assumes, what gets disclosed in the docs, stayed mine, checked against the spec I'd written first.

A few decisions I'd defend

Keys never touch the server, in either direction. For send links, the AES key lives only in the URL fragment. For receive links, the owner's ECDH private key lives only in the owner link's fragment, and every file an uploader drops gets a fresh AES key wrapped to the owner's public key via ECDH and HKDF before it leaves the uploader's browser. A stranger can seal a file to you without your browser ever being involved.

Padding is a real trade-off, not a free win. The server can't read file contents, but it can see ciphertext size and upload timing. That's unavoidable since expiry enforcement needs timestamps. I added three padding levels. Round to the next 4 KiB, which is basically free. Round to the next 1 MiB, which blurs same-size files. Or round to a fixed bucket like 10 MB or 1 GB, which wastes real bandwidth but hides size almost entirely. Standard is the default; most people don't need the third option, and the bandwidth cost isn't free.

Cloudflare Workers cap memory at 128 MB per request, and that shaped the whole file pipeline. You can't encrypt a 50 GB file in one in-memory buffer on a Worker, so files get chunked client-side with an independent AES-GCM tag per chunk, streamed through to R2 without fully landing in memory on either end. That early decision is the only reason the size limit could later go from 100 MB at launch to 200 GB without rewriting the upload path. Only chunk size and subrequest budget needed retuning.

What didn't go smoothly

An external security pass after the initial hardening milestone found things I'd missed despite writing the threat model myself. A handoff code ID the client could supply instead of the server generating it. A delete token sitting in a URL query string where it could leak into logs. CORS set looser than it needed on static assets. None catastrophic. All avoidable. Writing SECURITY.md doesn't mean you followed it everywhere; it means you have a doc to check yourself against, which is a different and lesser thing.

Separately, owner-side decryption for large files crashed with an out-of-memory error because I was holding the fully decrypted file as a Uint8Array instead of streaming it into a Blob. Client-side crypto doesn't mean client-side memory is infinite, and I'd apparently convinced myself otherwise for a few days.

What I learned

The crypto is the satisfying 20% of this project. The other 80% is abuse limits, expiry races, metadata leakage, and browser quirks. Safari and Firefox don't support BarcodeDetector for QR scanning, so the scanner needs a lazily-loaded jsQR fallback that Chrome never touches. None of that is glamorous, and all of it is where the hours went.

Staying simple is a decision you make repeatedly, not once. Every reasonable feature request, accounts, folders, a preview before download, pulls toward general-purpose cloud storage with extra steps. Saying no, on purpose, every time, is the job now.

AI pairing tools are good at structure, boilerplate, and test coverage. They are not a substitute for deciding what your threat model actually is. That judgment took the most thinking time and produced the fewest lines of code.

What SealDrop is today

It's live at sealdrop.io. Both flows work end to end. AES-GCM for files and metadata, ECDH P-256 plus HKDF for receive-mode key wrapping, chunked streaming up to 200 GB with resumable uploads, optional passphrase-protected links, three padding levels, and a handoff-code flow for moving a link from phone to laptop without retyping a URL. Builds are signed with an ECDSA key that only exists in CI, with checksums published so nobody has to take my word for it. The frontend, crypto, and shared packages are mirrored to a public repo. Server internals stay private, but the code that does the encrypting doesn't have to be trusted blind. It runs in English, Czech, and Macedonian, meets WCAG 2.1 AA, and ships a Docker Compose path for anyone who'd rather self-host. All of it sits inside Cloudflare's free tier.

Where I want to take it next

The current experiment is SealDrop Pro, a permanent receive link for accountants and tax advisers who need the same client-document intake every month instead of a one-off transfer, at a proposed €4 a month or €36 a year. I picked that niche partly because it's a market I can reach directly, in Czech, with a clear and recurring pain point. Right now it's outreach and short interviews. No payment collected, no feature built, because shipping fast and validating slowly are different skills, and I'd rather find out nobody wants to pay before I write a billing integration.

There's no usage dashboard worth screenshotting here, no growth chart. What I have is a tool I trust enough to send my own documents through, and a way to ask someone for a file without asking for their email address first. That was the point when I started, and it's still the only metric that matters to me on a Tuesday when someone actually uses it.