Fizzdoc 如何以無後端架構提供 74 種不需上傳檔案的工具
The architecture of a website that can't see your files (74 tools, no backend)
Fizzdoc 以無後端的靜態網站架構提供 74 種 PDF、Word、Excel、PowerPoint、圖像及音訊工具,並透過 Content Security Policy 限制頁面只能連往 fizzdoc.com,避免文件傳往其他伺服器。
Most "free online PDF tool" sites have the same architecture: your file goes up to their server, gets processed, and comes back. The privacy policy says they delete it after an hour. You have to take their word for it.
I learned this as the family's unofficial IT person. Every few weeks, someone sends me an Aadhaar scan or a bank statement and asks me to "make it smaller" or "unlock it". I kept uploading other people's documents to sites I knew nothing about, and at some point that stopped feeling okay.
After my first two posts on Fizzdoc, a close friend asked me some version of "but how do I know it really doesn't upload?" This post is the long answer.
So for Fizzdoc I wanted a site that can't see your file, by design. The browser itself won't let the page send it anywhere.
Fizzdoc now has 74 tools (PDF, Word, Excel, PowerPoint, images, audio), 16 languages and about 1,200 pages, and it has no backend at all. This post is about the design decisions that make that work, and the trade-offs I accepted.
Constraint 1: the page is not allowed to talk to anyone
Every page ships this Content Security Policy (in a <meta> tag, which the browser enforces just like a header):
default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self'; ...
connect-src 'self' is the important part. fetch, XMLHttpRequest, WebSockets, sendBeacon: the browser blocks all of them unless they go to fizzdoc.com itself, and fizzdoc.com is just static files. So even if one of the many open-source libraries had a bug or a malicious update, it couldn't send a document anywhere.
That one line shaped everything else:
- No CDNs. Fonts, WebAssembly engines and AI models are all served from the same origin.
- No analytics scripts. Even when Cloudflare injected its own analytics beacon, the CSP blocked it, because a script that isn't from the site can't run. (I switched that beacon off too; traffic numbers come from Cloudflare's server-side stats instead.)
- It is testable. The Playwright suite (about 1,250 browser tests) runs real jobs and fails if any request leaves the origin.
Constraint 2: no server, so the build does the work
There is no server to render pages, so a build step renders all of them ahead of time. One TypeScript file lists every tool (name, copy, FAQ, search keywords), and the build turns it into a static HTML page per tool per language: about 1,200 pages, each with its own title, structured data (FAQ, HowTo, breadcrumbs), hreflang links and sitemap entry.
The first load is small because each page only ships what it needs:
| On first load | Gzipped |
|---|---|
| JavaScript | about 19 KB |
| CSS | about 9 KB |
Everything heavy loads only when you use the tool that needs it, through dynamic import().
Each job gets its own throwaway worker
The PDF tools are qpdf compiled to WebAssembly. Every job gets a fresh Web Worker, which is terminated when the job ends. That keeps the page responsive, and the file and any password disappear with the worker.
Other engines follow the same pattern: pdf.js and pdf-lib for rendering and editing, Tesseract (WASM) for OCR, Whisper (through Transformers.js) for speech to text, and BiRefNet on ONNX Runtime Web (WebGPU when available, WASM otherwise) for background removal.
Big models on a host with a 25 MiB file limit
Cloudflare Pages refuses any file over 25 MiB. The background model is about 100 MB and the WebGPU build of ONNX Runtime is 27 MB.
So the build splits them into 20 MiB parts. On first use the worker downloads the parts, joins them, and keeps the result in Cache Storage. After that the tool starts instantly, and it works with the internet off. The build also fails if any output file goes over the limit, after one deploy failed on it.
Offline without breaking updates
A service worker makes every tool work offline after one visit. The rules are deliberately boring:
- Pages: network first, cache as a fallback. Online visitors always get the latest page.
-
Hashed assets (
/assets/index-abc123.js): cache first. A hashed file never changes, so the cached copy is always right. - Old files are never deleted. A page cached by an older version still needs its older assets.
- Models are excluded. Their tools manage their own caches.
A page that was never opened shows a small "needs the internet once" page in your language instead of the browser's error screen.
Hard parts that come from having no server
Phones run out of memory, silently. The first version of the background remover crashed Chrome on a 6 GB Android phone ("Aw, Snap!" at 5%). A tab gets far less memory than the phone has, and there's no error to catch. Phones now use a 4.6 MB model instead of the 100 MB one.
iPhone canvases have a size limit (about 16.7 million pixels). The Photo Editor tries full size first and falls back to the largest canvas the device allows.
Fonts couldn't be data: URLs. Vite inlines small files as data: URLs, my own CSP refused them, and the CSP-violation test caught it before it shipped.
"Share to WhatsApp" can't make a sticker. A web page can't add stickers to WhatsApp. A shared file arrives as a photo, so the page shows WhatsApp's own "Create sticker" steps instead of a button that lies.
The editor is just pixel maths
The newest tools, the Photo Editor and the Collage Maker, use no image library. Brightness, contrast, saturation and warmth are a short loop over the pixels. Every edit redraws from the original photo, so edits never pile up blur. Doing it in plain code instead of ctx.filter means every browser gives the same result, and unit tests can check it.
What it costs, and what 30 days of traffic look like
Fizzdoc runs on Cloudflare's free plan. Hosting costs me nothing, because every page and every AI model is just a static file. These are Cloudflare's own server-side numbers for the last 30 days (there is no analytics script on the site, so this is all I can see):
| Last 30 days | |
|---|---|
| Requests | 201k |
| Visits | 41.9k |
| Page views | 62.8k |
| Data served | 10.15 GB |
| Server errors (5xx) | 0% |
A few things I didn't expect:
- A big share is bots. Search crawlers (Googlebot, Applebot, Petalbot, Semrush) and AI crawlers show up all over the user-agent list. I welcome them in robots.txt, but it means "42k visits" is not 42k people. Treat these as upper bounds.
- Most of the bandwidth is AI models, not pages. The pages are tiny; the 10 GB is almost all background-removal and Whisper model files that visitors download once and then keep in their browser cache.
-
Bots keep looking for a backend that doesn't exist. Hundreds of requests for
/.env,/wp-config.php,/api/and/login. There is nothing to find, since there's no server, database or secrets behind the site. - Visitors come from everywhere. The United States, India, Singapore, South Korea, Australia and Germany lead, and the page is in 16 languages.
Trade-offs I accepted
- Your device does the work. A 300-page scan OCRs slower on an old phone than on a server.
- Some formats aren't possible yet. PowerPoint to PDF and old .doc/.xls files aren't supported.
- One-time downloads. The AI tools download their model once (the page shows the size first).
Try it, break it
- Site: https://fizzdoc.com
- Code (Apache-2.0): https://github.com/kingrishabdugar/fizzdoc
- Check it yourself: open DevTools → Network, run any tool, and watch nothing leave.
If you build something similar, the one decision I'd copy is the CSP. It turns "we don't upload your file" from a promise into a property of the page. And if Fizzdoc saved you an upload, a star on GitHub helps more people find it.
來源:DEV Community · dev.to





