Toolium

Notes on building a tool suite without a backend

Hemanth Gedda10 min read

When I started Toolium I assumed it would need a server. Not for everything. But for the harder cases - PDF, video, anything where the operation was non-trivial - my mental model was: client UI on the front, server compute on the back.

I was wrong. Across every tool on the site, exactly zero of them call out to a Toolium server. There is no Toolium server. There is a Vercel deployment that serves static HTML and JavaScript, an ads.txt file, an indexnow script that runs at build time, and that is the entire backend. Everything else runs in your browser.

This post is what I learned about why that works, where it breaks, and what would be different if I tried this in 2018 instead of 2024.

The libraries that did the heavy lifting

The architecture works because of a small set of JavaScript libraries that have quietly become world-class. Most of them are maintained by one or two people. None of them is famous outside their niche. All of them are production-grade.

pdf-lib (Andrew Dillon, 2020-present) does pure-JS PDF manipulation: merge, split, page extraction, metadata editing, form field manipulation. For "modify PDF structure" pdf-lib has no equal in browser land. The library is 600KB, which is large by browser-library standards and small by PDF-tool standards. It does not render PDFs - that is pdf.js's territory.

pdf.js (Mozilla, 2011-present) does browser PDF rendering. It is the library Firefox ships natively as its built-in PDF viewer. Open-source, MIT-licensed. Renders any PDF to a canvas at any resolution. The PDF-to-Image tool uses it; everything else uses pdf-lib.

SheetJS (Sheet5 Inc, 2012-present) handles every spreadsheet format that matters: xlsx, xls, ods, csv, tsv. Reads and writes. Works in Node and browser. Used in production by enough companies that the obvious bugs have been shaken out. The library was originally MIT and is now under a custom license that allows commercial use; Toolium uses the community edition.

browser-image-compression (Donald He, 2018-present) wraps the canvas image-encoding flow in a sane API. The library handles the kept-original fallback (when compression produces a larger file than the input, keep the input). Without that fallback, users would routinely see "compressed" outputs larger than their input and not understand why.

mammoth.js (Michael Williamson, 2014-present) parses .docx files. The cleanest library I have found for "extract structured content from Word" without dragging in a full Word renderer. Produces clean HTML or plain text.

Dagre powers the JSON Visualizer layout. It computes a directed-graph arrangement for the parsed structure, and the nodes and edges render on a custom pan-and-zoom canvas rather than a graph library. The result is the kind of structure-visualization UX that historically required a desktop app.

Beyond these, the rest of the tools use native browser APIs directly: Canvas for image work, SubtleCrypto for hashing, Intl.Segmenter for word counting, TextEncoder for byte conversions.

The patterns that emerged

Across the whole set, a small set of patterns repeats. The interesting ones:

Lazy-loaded library imports. pdf-lib at 600KB is not something you want on the home page. Each tool's bundle splits at the route boundary, and the library only loads when you visit the tool that needs it. Next.js handles the route-level splitting automatically; the only thing the tool code has to do is dynamic-import the library at the point of use.

Web Workers for heavy compute. Anything that could freeze the main thread runs in a worker. The Regex Tester is the canonical case: a catastrophic-backtracking pattern would hang the page indefinitely if the regex ran on the main thread, but in a worker we can terminate the worker after one second. The pattern generalizes to anything that takes more than ~16ms, which is one screen refresh.

File API plus Blob URLs for downloads. We never see the file bytes leave the page. The user picks a file via input or drag-drop, we read it into a Blob, the library processes the Blob, we write the output to a new Blob, we create a download URL via URL.createObjectURL, we trigger the download by clicking a hidden anchor. The whole flow is in browser memory.

Canvas API for image work. Resize, crop, format-convert, compress - all canvas. The trick is composing onto a white background before exporting to JPEG, otherwise transparent pixels become black (the bug I shipped a fix for early on).

Schema.org JSON-LD on every page. Each tool emits WebApplication schema; each blog post emits BlogPosting; the About page emits AboutPage and Organization. None of this affects the functional behavior, but it improves how the pages show up in search and how reviewers (human and automated) categorize the site.

Where the architecture breaks

Not everything works. The constraints I keep hitting:

Memory. A 100MB PDF will OOM Chrome on a 4GB machine. The browser does not have the unbounded swap of a server. The standard fix is to stream, but pdf-lib does not stream; it loads the whole document into memory. For PDFs larger than ~50MB the practical limit is the user's RAM.

Mobile. Operations that work fine on desktop fail on iOS Safari because of stricter memory limits. We catch these in testing, but every iOS update changes the limits. The PDF compression tool, in particular, has had to be retuned a few times.

OCR. Tesseract.js exists. It works. The English model is 10MB, and recognition takes 30-60 seconds per page with mediocre accuracy on real-world scans. Compared to Tesseract on a desktop (faster, more accurate) or ABBYY FineReader (much faster, much more accurate), the browser version is a clear downgrade. I have declined to ship OCR for this reason.

True video transcoding. ffmpeg.wasm exists. It works. It is 30MB to load. Encoding a 10-minute video takes 5-10 minutes in a tab. The UX is bad enough that I have declined to ship video tools.

Persistent state. There is no Toolium server, so there is no place to save your work between sessions. localStorage and IndexedDB exist, but they are scoped to the browser-tab origin and get cleared when the user clears their history. For tools where state matters - an invoice generator that should remember your business info - the lack of cross-session persistence is a real limitation.

The 2018 comparison

What would be different in 2018? Most of it. pdf-lib's first stable release was 2019. browser-image-compression came in 2019. SubtleCrypto was supported but the algorithms (SHA-256, SHA-512) were inconsistent across browsers until around 2017. Web Workers worked but the developer ergonomics were rough, and the support story across iOS Safari was patchy.

The infrastructure changes that made Toolium possible were not 2024-specific. They mostly happened between 2018 and 2022. I just took longer to notice. The "you need a server for that" assumption persisted in my head years after it stopped being true in practice.

The conclusion

Two years in, my conclusion is that "browser-only" is the right architecture for any utility tool that does not need persistent storage or third-party API integration. The remaining cases for server-side compute are narrower than I expected:

  • Sub-millisecond response on a slow client (server is faster than the user's CPU)
  • Multi-gigabyte file streaming (browser is bad at this)
  • AI inference at scale, where the models are too large to ship to clients
  • Anything requiring shared state across users (collaborative editing, social features)

For everything else, the browser is the right place. The "free online X" sites that still operate server-side are doing it from inertia, not because the architecture requires it. The lock-in of "we already have the infrastructure" is real, but a greenfield project building one of these tools today should start in the browser and only move to a server when something genuinely cannot be done locally.

The Toolium experience suggests that "something" comes up much less often than I expected.