Engineering

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.

Marek Novák · Engineering LeadFebruary 25, 2026 10 min read
Share Post LinkedIn
Building a zero-upload SaaS: architecture notes

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.

MN

Marek Novák

Engineering Lead at nctools