DEV Community

Lakhyajit Changmai
Lakhyajit Changmai

Posted on

I grepped my own app for file-upload paths. There is exactly one, and it is the contact form

The claim, stated precisely

ZANCTA is a browser-based PDF and image tool suite: 17 tools, all defined in one registry file. The claim on the landing page is not "we do not store your files". It is stronger and much more checkable: no tool ever sends file bytes to a server.

That is falsifiable with a text search, and every app with a privacy claim on its landing page should run the search.

Step 1: enumerate every way bytes could leave

Upload primitives in a browser app are few and boring:

  • new FormData() sent via fetch or XMLHttpRequest
  • a File or Blob passed as a fetch body
  • navigator.sendBeacon
  • base64-encoding a file into a JSON field (the sneaky one)
  • an input type=file whose contents get read and posted

Grepping the app surface for the first three produced exactly one hit:

components/contact/contact-form.tsx:31 - const data = new FormData(form)

Text fields only: name, email, message. No file input in that form.

Then the sneaky one. FileReader and arrayBuffer() calls are everywhere, because every tool has to read its own files. The question is what happens after the read. The answer, here: pdf-lib, pdfjs-dist, browser-image-compression, libheif-js, tesseract.js, onnxruntime-web, qrcode, spark-md5, and a handful of Web Workers. All client libraries. That is the entire processing surface.

Step 2: audit the server side

35 route files under app/api/**/route.ts. Sorted by what they touch: auth and session, account deletion, entitlements, payments, admin SEO integrations, analytics, contact, health, OCR language delivery, IndexNow. None of them accept a file. Zero handlers call req.formData() or read an arrayBuffer.

One group looks alarming if you only read the names. Premium adds six OCR languages plus scanned-PDF OCR, and Tesseract needs .traineddata files to do that:

app/api/ocr/lang-token/route.ts
app/api/ocr/lang/[...file]/route.ts
app/api/ocr/status/route.ts

The handler is GET only. The server issues a token, then serves the language pack down to the browser. Your document never goes up. The direction of the arrow is the whole claim.

Step 3: the route that does receive something about your files

One analytics endpoint records that a tool run completed. It is the only server route that learns anything about your processing, and its guardrails are worth stealing:

  • It accepts one field: a tool slug, validated against an allowlist built from the tool registry.
  • MAX_BODY_BYTES is 512, checked against both the Content-Length header and the actual parsed payload. The comment in the code says why: reject anything larger rather than parsing a payload that could carry file data.
  • Cross-origin requests are rejected before parsing.
  • Anonymous calls are stored with userId null and no IP or user-agent, so the row cannot identify a person.
  • Extra body fields are read and discarded, never stored.
  • Analytics failures are swallowed so a telemetry bug can never surface as a tool failure.

That last group is the interesting bit: the size cap is not hygiene, it is the privacy guarantee. A claim enforced by "we promise not to send more" is a claim enforced by nothing.

Step 4: what running locally actually costs

Nothing is free. Local processing buys privacy by moving weight onto the client:

  • public/models/u2netp.onnx is 4,574,861 bytes - the background-removal model, fetched before the first removal works.
  • public/ocr/ holds 14 WASM and worker assets.

So the honest phrasing is: your file bytes are not uploaded. Not: the app needs no network. It loads scripts, models, and consent-gated analytics. Anyone who writes "100% offline" and "no tracking whatsoever" on a landing page is writing copy, not engineering.

Step 5: the audit caught two real false statements

Both were in my own documentation rather than the code:

  1. README.md and CHANGELOG.md say "Eleven local PDF and image tools" and then list eleven. The registry has seventeen. The docs were six tools stale.
  2. README.md and the llms.txt route both say background removal is "PLANNED - deferred: no model and no cloud fallback". A 4.5 MB ONNX model ships in public/models/ and the component is fully implemented. Both statements are false today.

Neither was malicious. Both are exactly how privacy claims rot: someone writes a summary at the moment it happens to be true, the code moves, nobody re-reads the summary.

There is also a live contradiction I have not resolved. This branch adds background-remover to a non-indexable list, so a tool that works is deliberately kept out of the sitemap. That is a defensible discovery decision, but it means "what the site advertises" and "what the app does" now disagree on purpose - and I have to pick one before the next public description goes out.

What the audit lets me publish

Checkable: 17 tools, all processing in the tab; one FormData in the app and it belongs to the contact form; every API route accounted for by name; OCR weights served down instead of documents collected up; a 512-byte cap on the only telemetry route that touches processing.

Not checkable, and competitors do it constantly: user counts, "trusted by", accuracy percentages, "unhackable", "completely anonymous". None of those have a grep.

If your landing page makes a privacy claim, run the search this afternoon. The distance between what the page says and what the code does is where trust gets spent.

Top comments (0)