Building a zero-upload SaaS: architecture notes
How we structure a product where 90 percent of user activity never touches our server, and what that means for our monitoring, billing and support.

Most SaaS products live on their servers. User requests come in, business logic runs on the backend, responses go out. Metrics and logs pile up. Errors get grouped by request ID. It is a comfortable, well-understood pattern.
nctools is different. Our conversion features run entirely in the user's browser. We see the sign-in, the page view, the button click — but never the file. That is intentional, and it forces a different architecture.
What we do observe
We observe events, not payloads. When a user runs a PDF-to-Word conversion, we log:
- the tool name (pdf-to-word)
- the page count of the source (a number, not the file)
- the wall-clock duration (in ms)
- success or failure (a boolean and an error class if failure)
- the browser and OS (from User-Agent)
We never log the filename, the file's contents, or any user-provided metadata. If a conversion fails, we get the error class ('font-decode-failed') but not the failing file.
Error reproduction is harder
A traditional SaaS support flow: user reports bug → engineer looks at their request in the logs → engineer reproduces from that data. We cannot do that. When a user reports a failed conversion, we ask them to share the file voluntarily (never automatically) and reproduce locally.
This is slower per-incident but leads to better bug reports. Users learn that our conversion is local, so their bug report contains details a server-based flow would never elicit — 'this PDF has scanned Arabic text on pages 40-50', 'the file was originally a Keynote export via Preview'.
Billing without seeing usage
Our plans are per-seat, not per-file. This is the natural fit for a local-first product. A power user running 1000 conversions per day and a light user running one both count as one seat. There is no server-side counter to enforce.
On paid plans, we do track a monthly 'conversion count' — but the count is incremented by the client after each conversion, over an authenticated request. The server sees a POST /usage with a workspace ID and an integer. It never sees the file.
Trust model
This system trusts the client to report honestly. We accept the trade-off because our per-seat pricing means gaming the counter offers no financial upside — it just changes a number in your own dashboard.
The support portal
Our support portal has a 'Share a file' upload endpoint that is opt-in per-incident. Users get an email link that expires in 48 hours. Uploaded reproductions are encrypted at rest, only accessible by engineering on a specific ticket, and deleted the moment the ticket closes.
Would we do it again?
Yes. The local-first architecture has cost us some things — slower bug reproduction, more infrastructure per-feature (each tool ships its own decoder) — but it has bought us differentiation. Legal teams, medical practices, and government departments buy from us specifically because our servers cannot see their files.
Marek Novák
Engineering Lead at nctools
Keep reading
Engineering
OCR in the browser: how Tesseract.js and WebAssembly changed everything
A deep look at running the world's most-used open-source OCR engine at 200+ MB of trained models directly in a user's tab.
ReadEngineering
Client-side PDF extraction with pdfjs-dist: a deep dive
Mozilla's pdfjs-dist library powers every PDF reader on the web. Here is what it does well, where the sharp edges are, and how we build on top of it.
ReadEngineering
PDF to Excel — heuristics for table detection in the wild
PDFs describe positions, not tables. Turning that into rows and columns is an ancient computer-science problem. Here is how we approach it.
Read