Every "free online file converter" on the internet is secretly one of two different machines. They look identical in search results. Both have a drop zone, a progress bar, and a download button. But under the hood, one is a thin frontend for somebody's server — your file takes a trip, gets processed next to who-knows-what-else, and (if you're lucky) gets deleted "within 24 hours". The other kind is your own computer doing the work, wearing a website as a disguise.
I run toolfyra, a toolkit built entirely as the second kind, and this post is the guided tour of how that architecture actually works: the four browser APIs doing the heavy lifting, where the honest limits are, and — most usefully — how you can verify any tool's "no upload" claim yourself in about thirty seconds with DevTools.
1. File.arrayBuffer() — the file becomes bytes, locally
When a user drops a file onto a page, the browser hands JavaScript a File object. Crucially, that's a reference to a file on your disk, not a copy on a server. Reading it into memory is one call:
input.addEventListener('change', async () => {
const file = input.files[0];
const buf = await file.arrayBuffer(); // bytes, in RAM, on YOUR machine
const bytes = new Uint8Array(buf);
console.log(`${file.name}: ${bytes.length.toLocaleString()} bytes`);
});
That's the modern replacement for the old FileReader + onload callback dance, and it's the moment every client-side tool begins. Nothing has been transmitted. If the page crashed right here, your file would be exactly where it always was.
2. Web Workers — heavy work without freezing the tab
Parse a 50 MB PDF on the main thread and the UI locks up: every click, every scroll, dead until the parse finishes. The fix is to move the work into a worker thread — and while you're at it, transfer the buffer instead of copying it:
// main.js
const worker = new Worker('/js/pdf-worker.js', { type: 'module' });
worker.postMessage({ buffers: [bufA, bufB] }, [bufA, bufB]); // transfer list = zero-copy
worker.onmessage = ({ data }) => {
const url = URL.createObjectURL(new Blob([data], { type: 'application/pdf' }));
const a = Object.assign(document.createElement('a'), { href: url, download: 'merged.pdf' });
a.click();
URL.revokeObjectURL(url);
};
// pdf-worker.js — runs off the main thread, UI stays responsive
import { PDFDocument } from 'pdf-lib';
self.onmessage = async ({ data: { buffers } }) => {
const out = await PDFDocument.create();
for (const buf of buffers) {
const doc = await PDFDocument.load(buf, { ignoreEncryption: true });
const pages = await out.copyPages(doc, doc.getPageIndices());
pages.forEach((p) => out.addPage(p));
}
const bytes = await out.save();
self.postMessage(bytes.buffer, [bytes.buffer]); // hand the result back, also zero-copy
};
The second argument to postMessage is the transfer list. Without it, structured clone copies the buffer; with it, ownership moves. For a 500 MB buffer that's the difference between instant and "why is my fan spinning".
3. WebAssembly — the part that made this whole category possible
Ten years ago the story would end here, because the browser didn't have real parsers for most file formats. WebAssembly changed the ledger. Battle-tested libraries — compiled from C/C++/Rust or shipped as plain JS — now run in the page's sandbox: pdf-lib and pdf.js for PDFs, ffmpeg.wasm for audio and video, Tesseract.js for OCR. The server-free tool category exists because these ports exist. The same merge engine that powers desktop apps runs in your tab, with no server and no queue.
4. File System Access API + OPFS — when RAM isn't enough
Holding everything in memory works up to a point. For bigger jobs, modern Chromium gives the page streaming access: window.showOpenFilePicker() returns a handle you can read in chunks, and the Origin Private File System (OPFS) provides a browser-managed scratch space with its own quota. The pattern is feature detection, not user-agent sniffing:
if ('showOpenFilePicker' in window) {
// streaming path: read slices, write chunks to OPFS as you go
} else {
// fallback: ArrayBuffer + Blob, same math, smaller ceiling
}
The honest limits
Client-side processing is not magic, and any tool that pretends otherwise is selling something. Memory is the big one: a server streams a 2 GB file; a browser holds it in RAM next to every other tab you have open. Mobile Safari adds its own caps — the File System Access API isn't there yet, and iOS is aggressive about killing pages that allocate too much. GPU-class work (OCR across a 500-page scan, video transcodes) is still often faster on real hardware that isn't your laptop. And "works offline" only applies after the first load, because the WASM binaries themselves need one cached fetch. Good tools feature-detect, degrade gracefully, and tell you the ceiling instead of pretending there isn't one.
Verify the claim yourself: the 30-second audit
This is the part worth bookmarking. "Files never leave your browser" is not a promise you have to believe — it's an architecture claim you can test:
- The Network tab. Open DevTools → Network, filter to Fetch/XHR, run the job. Local processing means no request body ever carries your file. If you see a multi-megabyte POST, you just watched your file leave.
- The airplane test. Load the tool, disconnect from the network, run a job. If it completes offline, nothing external was involved. (Load the page first — the WASM payload needs one cached fetch.)
-
Read the source. Search the bundle for
fetch(,XMLHttpRequest, andsendBeacon(. A local-only tool uses them for assets and analytics, never for file payloads. - The retention sentence. The privacy page should describe what happens to your file in plain words. "Processed locally in your browser" is testable now. "Deleted within 24 hours" is a promise about a server you can't see.
Why this is the whole point
Zero-retention is an architecture property, not a policy checkbox. For a public menu PDF, upload-based processing is fine. For a contract, an ID scan, a payslip, or a medical report, the only way to guarantee a file never leaks from a server is for there to be no server in the loop. That's the design principle this entire category — and my toolkit — is built on.
Everything above is how toolfyra.com's PDF tools work: merge, split, fill forms, OCR, page numbers — no accounts, no uploads, and a Network tab that stays boring.
Disclosure: toolfyra.com is my own project. This post is part of a series about building it in public — the architecture, the data, and the occasional embarrassing bug.
Top comments (0)