A couple of years into running small web tools, I got tired of the same pattern: every "free PDF tool" site either watermarked my file, throttled me after two uploads, or — worse — quietly sent the document to a server I didn't control. For something as sensitive as a tax form or a contract, that's a real problem.
So I built the opposite. UnTrackedTools is now 90+ tools (PDF, images, JSON, text, archives, dev utilities) that run entirely in your browser. The file never leaves the device. There's no upload step, no account, no backend processing the bytes.
This post isn't a product pitch. It's a walkthrough of how that architecture works in practice, what libraries do the heavy lifting, and the parts that bit me along the way — because "client-side" sounds clean until you hit a 400 MB PDF on a phone.
The mental model: file in, file out, nothing in between
Every tool follows the same shape:
<input type="file">
↓ File API reads bytes into memory (ArrayBuffer / Blob)
↓ a library mutates that in-memory data
↓ we hand the result back as a download
No fetch() to a server with your file. The libraries I use literally can't phone home with your data — they operate on ArrayBuffers sitting in tab memory.
Here's the actual skeleton most of the tools share:
const input = await file.arrayBuffer(); // bytes live in the tab now
const output = await transform(input); // pure, in-memory
const blob = new Blob([output]);
const url = URL.createObjectURL(blob);
// trigger a download, then revoke the URL
That's the whole privacy story: the bytes are in the tab, the transform is synchronous local code, and the only thing that ever crosses the network boundary is the static JS bundle itself.
What actually does the work
I'm not reinventing file formats. I lean on battle-tested libraries that happen to run in the browser:
-
PDF →
pdf-lib. Merge, split, rotate, add page numbers, compress — all by reading the PDF into memory and rewriting it client-side. The compressor is honest about its limits: it repackages the PDF with object streams (useObjectStreams), it doesn't re-encode embedded images. So a scanned PDF (already a flat image) won't shrink much. That's a real constraint, not a bug, and I say so on the tool page. -
Images →
compressorjs, which re-encodes through the Canvas API. No server round-trip, just the browser's own image pipeline. -
Archives →
JSZipfor zipping files together in memory. -
Passwords / random → the Web Crypto API (
crypto.getRandomValues). This matters more than people think: a password generator built onMath.random()is generating guessable passwords. Mine pulls from the OS cryptographically-secure RNG.
If you want to see any of those in action, here are three that map directly to the libraries above:
- Browser PDF compressor — pdf-lib, repackages locally
- Image compressor — Canvas re-encode, no upload
- Password generator — Web Crypto, client-only
The parts that bit me
"Client-side" is easy to say and harder to ship well. Three things caught me out:
1. Big files block the main thread.
Most of my transforms run on the UI thread. A 50 KB JSON formatter is instant. A 200-page PDF merge is fine. But push a 300 MB file and the tab freezes while the library churns — the user sees "page not responding" and assumes it's broken. The honest fix is a Web Worker so the parse happens off-thread, and I haven't rolled that out to every tool yet. I'm naming it because pretending it's all smooth would be a lie.
2. Browser memory is the real ceiling.
An ArrayBuffer lives in RAM. Desktop browsers handle a few hundred MB comfortably; mobile Safari gets unhappy much earlier, and there's no clean "out of memory" error — it just kills the tab. So the practical limit isn't my code, it's the device. I surface a size warning on the heavy tools instead of promising the moon.
3. Safari is its own country.
File System Access API (showSaveFilePicker) would let me write the result back without the download-dialog dance. Chrome ships it; Safari largely doesn't. So I target the lowest common denominator — generate a Blob, trigger a download — and it works everywhere, just one extra click on Apple devices. Not elegant, but it doesn't fail.
Why "private by architecture" beats "private by promise"
Most sites say they delete your file after an hour. You're trusting a server you can't see. The architecture here removes the question: there is no server step that touches your file, so there's nothing to delete, log, or leak. Privacy isn't a setting I flip on — it's a consequence of where the bytes live.
That also means no compliance theater, no "we never share your data" footer clause to litigate. The data never arrived.
If you're building something similar
The short version:
- Reach for
pdf-lib,compressorjs,JSZip, and Web Crypto before you write a byte of format code yourself. - Treat
crypto.getRandomValuesas the only acceptable RNG for anything security-related. - Plan for the main-thread freeze on large inputs — a Worker is the right call, I just owe you that work.
- Test on mobile Safari early. It will be the constraint.
The fun part of this approach is that the "feature" (privacy) is free once the architecture is right. The hard part is everything around it: honest limits, memory ceilings, and browser quirks nobody puts in the landing-page copy.
If you want to poke at the tools or tell me which one should get a Web Worker first, the whole collection lives at untrackedtools.com.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.