<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tasin Hasan</title>
    <description>The latest articles on DEV Community by Tasin Hasan (@tasin01311).</description>
    <link>https://dev.to/tasin01311</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4139774%2F9830469d-eab8-4118-8571-4a34953ac507.png</url>
      <title>DEV Community: Tasin Hasan</title>
      <link>https://dev.to/tasin01311</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tasin01311"/>
    <language>en</language>
    <item>
      <title>I built 49 document tools that never upload your file</title>
      <dc:creator>Tasin Hasan</dc:creator>
      <pubDate>Wed, 23 Sep 2026 16:14:30 +0000</pubDate>
      <link>https://dev.to/tasin01311/i-built-49-document-tools-that-never-upload-your-file-4nk1</link>
      <guid>https://dev.to/tasin01311/i-built-49-document-tools-that-never-upload-your-file-4nk1</guid>
      <description>&lt;p&gt;Every "free PDF tool" site works the same way: you upload your document to someone's server, they process it, you download the result. You are trusting a stranger with a contract, a payslip, a medical form, and the privacy policy that governs it is usually three paragraphs of nothing. 😬&lt;/p&gt;

&lt;p&gt;I wanted to know how many of those tools actually &lt;em&gt;need&lt;/em&gt; a server. The answer turned out to be: most of them don't. 49 out of 59, in my case.&lt;/p&gt;

&lt;p&gt;Open Doc Tools has 59 tools. 49 of them run entirely in your browser tab. The file is never uploaded, because there is nothing to upload to. 🔒&lt;/p&gt;

&lt;h2&gt;
  
  
  🧰 What runs client-side, and with what
&lt;/h2&gt;

&lt;p&gt;Most of it is unglamorous library work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pdf-lib&lt;/strong&gt;: merge, split, rotate, crop, page numbers, watermarks, form filling. Everything structural.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pdfjs-dist&lt;/strong&gt;: rendering pages to canvas for previews, extracting text, and reading PDF attachments (which is how the e-invoice viewer pulls the XML out of a ZUGFeRD hybrid PDF).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tesseract.js&lt;/strong&gt;: OCR. The language data is ~10 MB per language, fetched on demand when the user picks one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/imgly"&gt;@imgly&lt;/a&gt;/background-removal&lt;/strong&gt;: a segmentation model running on ONNX Runtime Web via WebAssembly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;exceljs / docx / pptxgenjs / mammoth&lt;/strong&gt;: reading and writing Office formats without touching a server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;upscaler + tfjs&lt;/strong&gt;: image upscaling with ESRGAN.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser is a genuinely capable document runtime now. The gap between "needs a server" and "doesn't" is much smaller than the market implies.&lt;/p&gt;

&lt;h2&gt;
  
  
  ☁️ What genuinely does need a server
&lt;/h2&gt;

&lt;p&gt;Ten tools do. Mostly format conversions that need a real office suite: DOCX to PDF, XLSX to PDF, PPTX to PDF. Those run LibreOffice headless in an isolated container, alongside qpdf and Ghostscript.&lt;/p&gt;

&lt;p&gt;These are the only tools where your file leaves your device, and that is exactly why they are labelled differently from the rest.&lt;/p&gt;

&lt;p&gt;I could have hidden this. Instead every tool carries a badge saying where it runs, and the honest number, 49 of 59, is in the page title. A privacy claim that quietly excludes one tool in six is not a privacy claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛡️ The Content Security Policy fight
&lt;/h2&gt;

&lt;p&gt;This is the part that cost me the most time, and the part nobody warns you about.&lt;/p&gt;

&lt;p&gt;A strict CSP and WebAssembly do not get along. Compiling WASM needs &lt;code&gt;'wasm-unsafe-eval'&lt;/code&gt; in &lt;code&gt;script-src&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;script&lt;/span&gt;-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is narrower than it sounds. Unlike &lt;code&gt;'unsafe-eval'&lt;/code&gt;, it cannot turn a string into code, only compile WASM. But without it the background remover fails, and it fails &lt;em&gt;only in production&lt;/em&gt;, because the dev server ships no CSP. That is a fun afternoon. 🙃&lt;/p&gt;

&lt;p&gt;&lt;code&gt;blob:&lt;/code&gt; is there for a second reason: the ONNX runtime dynamically imports a module it generates at runtime from a blob URL. A blob script can only be created by code already running on your own origin, so this does not open the door to a third party. But you have to know that before you feel good about adding it.&lt;/p&gt;

&lt;p&gt;Yes, &lt;code&gt;'unsafe-inline'&lt;/code&gt; is in there too. It is not ideal, and I would like to tighten it.&lt;/p&gt;

&lt;h2&gt;
  
  
  🐛 The bug that taught me the most
&lt;/h2&gt;

&lt;p&gt;I split money handling into two modules: one with &lt;code&gt;node:fs&lt;/code&gt; for persistence, one pure for formatting. Then I imported the wrong one into a client component.&lt;/p&gt;

&lt;p&gt;The build passed. The page rendered. And then it crashed in the browser with &lt;code&gt;h(...).join is not a function&lt;/code&gt;, because Next had bundled a server-only module into the client, and &lt;code&gt;node:fs&lt;/code&gt; does not exist there.&lt;/p&gt;

&lt;p&gt;The lesson I actually took from it: "shared utility module" is a smell when half of it is server-only. Split by &lt;em&gt;where it runs&lt;/em&gt;, not by &lt;em&gt;what it is about&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚠️ What doesn't work well
&lt;/h2&gt;

&lt;p&gt;Client-side is not magic, and I would rather tell you than have you find out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Big files are slower.&lt;/strong&gt; Your device is doing the work, not a data-centre CPU. Large scanned PDFs through OCR take a while.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-end phones can struggle.&lt;/strong&gt; Heavy tools like background removal and upscaling need a fair amount of memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCR needs a download first.&lt;/strong&gt; The ~10 MB language pack has to arrive before the first run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some conversions can't be done in a browser.&lt;/strong&gt; That is the ten server-side tools above.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  💡 What I would do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decide the CSP first.&lt;/strong&gt; Retrofitting one around WASM, blob workers and third-party scripts is far more painful than starting strict and widening deliberately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count the marketing claims in code.&lt;/strong&gt; The "49 of 59" on my homepage is computed from the tool registry, so it cannot drift from what actually ships. I added that after nearly publishing a wrong number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the region-dependent paths.&lt;/strong&gt; Consent defaults that differ by country are invisible from wherever you happen to be sitting.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;It is at &lt;a href="https://opendoctools.com" rel="noopener noreferrer"&gt;opendoctools.com&lt;/a&gt;: free, no account, no watermarks. Happy to answer anything about the implementation. 🙌&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>webassembly</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
