I'm an IT technician in Quebec. During the day, I fix other people's computers. A surprising amount of that job comes down to "I just need to merge two PDFs" or "how do I shrink this photo so the portal accepts it?"
For years my answer was "google it and pick a site". Then I started paying attention to what a lot of those sites actually do: ads crowding the upload button, "free" tools that turn into a subscription after a file or two, tight size limits, and, worst of all, people uploading pay stubs, IDs and medical forms to servers they knew nothing about.
So on evenings and weekends I started building my own. That side project is now WebFileTools: 184 free tools in French and English, covering PDF, image, video, audio and developer utilities. No ads, no account, hosted in Quebec. I'm building it solo, and this post covers how it works and what was harder than I expected.
The one rule: browser first, and be honest when it isn't
Before writing much code, I set one rule for myself:
If a tool can run in the browser, it runs in the browser. If it needs a server, the page says so clearly, and the file is deleted right after.
I'll be upfront: not every tool is local, and I don't pretend otherwise. Most of them are, but things like strong PDF compression, Office-to-PDF conversion or scanned-PDF OCR are just much better done server-side with mature native tools.
So every tool in the catalog is tagged local or server, and each tool page shows that as a badge. If you see "local", your file never leaves your device. If you see "server", it goes over HTTPS to a server in Quebec, gets processed, and is deleted afterwards.
The stack
Nothing exotic, and I'd rather keep it boring:
-
Next.js (App Router) for the whole site, with one dynamic
[tool]route per language rendering every tool page. - Cloudflare in front, for TLS, caching and a first layer of abuse protection.
- Local tools: FFmpeg.wasm for audio/video, pdf-lib for PDF page editing (reorder, crop, redact, metadata), and plain Canvas for images (resize, enhance and so on).
- Server tools: Ghostscript (PDF compression, PDF/A), LibreOffice (Office documents to PDF), qpdf (PDF unlock/repair), Tesseract (OCR on scanned PDFs) and ClamAV to scan files on the file-sharing tools.
- Umami for analytics, served from my own subdomain.
A strict Content-Security-Policy (script-src 'self' plus 'wasm-unsafe-eval', worker-src 'self') means the wasm and the workers are served from my own domain, not pulled from a random CDN at runtime.
Example: trimming audio entirely in the browser
This is roughly what a local tool like the MP3 cutter does. It's a simplified example, not my exact production code:
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile } from "@ffmpeg/util";
const ffmpeg = new FFmpeg();
export async function trimAudio(file, start, end) {
if (!ffmpeg.loaded) {
// core files are self-hosted so the CSP can stay 'self'
await ffmpeg.load({
coreURL: "/ffmpeg/ffmpeg-core.js",
wasmURL: "/ffmpeg/ffmpeg-core.wasm",
});
}
const ext = file.name.split(".").pop();
const input = `in.${ext}`;
const output = `out.${ext}`;
await ffmpeg.writeFile(input, await fetchFile(file));
// -c copy: no re-encode, keeps the original format and is fast
await ffmpeg.exec(["-ss", start, "-to", end, "-i", input, "-c", "copy", output]);
const data = await ffmpeg.readFile(output);
// free wasm memory right away, it adds up fast
await ffmpeg.deleteFile(input);
await ffmpeg.deleteFile(output);
return new Blob([data], { type: file.type });
}
No upload and no queue. The "server" for this tool is your laptop.
Example: a server tool that cleans up after itself
For server tools, the pattern is always the same: a temp directory per job, run the native tool with a timeout, and finally delete everything. Here's an illustrative version of a PDF unlock with qpdf:
import { execFile } from "node:child_process";
import { mkdtemp, writeFile, readFile, rm } from "node:fs/promises";
import { promisify } from "node:util";
import { tmpdir } from "node:os";
import path from "node:path";
const run = promisify(execFile);
export async function unlockPdf(buffer, password) {
const dir = await mkdtemp(path.join(tmpdir(), "job-"));
const input = path.join(dir, "in.pdf");
const output = path.join(dir, "out.pdf");
try {
await writeFile(input, buffer);
// execFile with an args array: no shell, no injection via the password
await run("qpdf", [`--password=${password}`, "--decrypt", input, output], {
timeout: 30_000,
});
return await readFile(output);
} finally {
await rm(dir, { recursive: true, force: true }); // always, even on error
}
}
Fun detail: on the PDF protection tool, both protecting and unlocking go through the server, and the page spells that out, because "your file and password travel to a server" is something people deserve to know before clicking.
The hard parts (and what I learned)
1. Wasm memory and large files. FFmpeg.wasm is amazing, but the browser isn't a server. Everything lives in a wasm memory space, so a big video means the input, the output and FFmpeg's own buffers all sit in RAM at the same time. On a phone, that crashes the tab without any useful error. What helps: stream-copy (-c copy) whenever possible instead of re-encoding, deleting files from the virtual FS the moment they're no longer needed, loading the core lazily (only when the user actually drops a file), and being honest in the UI about realistic sizes instead of letting people wait for a crash.
2. Bilingual SEO is two websites, not one. Quebec is French-first, so French is the default and English lives under /en/. I learned quickly that translating the slug matters: /decoupeur-mp3 and /en/mp3-cutter are separate pages, linked with hreflang alternates and their own canonical. A French speaker searches "découper mp3", not "mp3 cutter", so a translated title on an English URL doesn't cut it.
3. Keeping 180+ pages unique. It's very tempting to generate 184 pages from one template and call it a day. Google is not impressed by that, and neither are users. Each tool now has its own description, use cases, FAQ and a short explanation of how it processes your file (local vs server, and with what). Writing that content in two languages is the least glamorous part of the project, but it's also what makes a page actually useful.
4. Abuse control without accounts. Local tools cost me nothing, so they're uncapped. Server tools cost real CPU, so the free tier is one file at a time with a cap of 25 server runs per day. Combined with Cloudflare in front, the goal is to stop one script from eating the whole server without forcing everyone to sign up.
5. Why no account. An account would make rate limiting easier and give me an email list. But "create an account to merge two PDFs" is exactly the thing I was annoyed by in the first place. Also, every piece of data I don't collect is data I don't have to protect.
What's next
There are no ads and no investors, so I'm working on a Pro plan (€6.99/month) for people who need batch processing, bigger files (up to 500 MB) and unlimited server runs. It isn't for sale yet: it's waitlist-only for now, because I'd rather validate that people want it before taking anyone's money. The free tier isn't going away, and local tools stay uncapped.
Mostly, though, I'd love feedback from this community:
- Which tool do you wish existed that runs fully in the browser?
- Any tips for dealing with wasm memory limits on mobile browsers?
- Does the local/server badge make sense to you, or would you want more detail?
A few tools to try
- MP3 cutter: local, FFmpeg.wasm, no upload
- Encoding converter: fixes mojibake in CSV/text files (UTF-8, Latin1, Windows-1252), in the browser
- File drop: server-side, time-limited share link (1 h to 7 days), ClamAV scan, purged on expiry
- One-time secret file: single-use encrypted link (AES-256-GCM, up to 25 MB)
Or browse all 184 from the homepage linked at the top. Thanks for reading, and feel free to tell me what's broken. I'm one person, so there's definitely something. 🇨🇦

Top comments (2)
180 tools as a solo dev is a lot to keep running. How do you decide what to fix first when several break at once?
Honestly, by impact. If a tool that people actually use breaks (PDF compression, image resize, conversions), it goes first. The server-side tools (Ghostscript, LibreOffice, qpdf) are the ones that break most, so they get checked first. The browser-only tools rarely break once they work. Then I batch the small UI bugs. What helps most is that the tools share the same upload/download components, so one fix often covers dozens of pages.