Skip to content
Case study · Interface.ai → open source

A document viewer that handles everything

From an internal library at a banking company to two published npm packages: doclens and the PDFium-powered pdfnova.

14+

file formats, one component

Role

Author and maintainer

Timeframe

2025 – present

Stack

TypeScript · WebAssembly · PDFium · Tesseract.js · React

The problem

Interface.ai's platform needed to render customer-uploaded documents — PDFs, spreadsheets, presentations, exports of every vintage — inline, with search and highlighting. The existing options were bloated, poorly maintained, or handled only a fraction of the formats banks actually upload.

Every product team was about to solve document rendering separately. That's the moment to build a library.

The constraints

  • Format diversity was non-negotiable: banks upload PDF, XLS, XLSX, PPT, XML, JSON, HTML, and CSV, and the viewer had to handle all of them credibly.
  • PDF rendering quality had to match what users see in Chrome — anything less reads as broken to a non-technical user.
  • Bundle size mattered: product teams would not adopt a viewer that added megabytes to their routes unconditionally.

Decisions & trade-offs

A dedicated PDF foundation: pdfnova on PDFium via WebAssembly

Instead of building on pdf.js, I compiled PDFium — Chrome's own PDF engine — to WebAssembly and wrapped it in a typed JavaScript API: rendering, text extraction, search, annotations, forms, and digital signatures. doclens consumes it as its PDF backend.

Trade-off: The WASM engine adds about 2MB to the PDF path. I took the size hit in exchange for rendering fidelity, and kept it isolated so only PDF consumers pay it.

Plugin architecture with tree-shaking as the contract

Each format is an adapter behind a common interface. Apps import only the formats they need; unused adapters are shaken out of the bundle. The core stays around 6 KB.

Trade-off: A plugin system is public API surface — every adapter interface I expose is a compatibility promise to maintain. The alternative, one monolithic viewer, would have been simpler to build and worse to adopt.

OCR as a first-class feature, not an afterthought

Scanned documents are common in banking. doclens integrates Tesseract.js so full-text search works even when the document is just images of text.

Trade-off: OCR is slow and memory-hungry, so it runs opt-in and off the main thread — full-text search on scans costs seconds, clearly signposted, rather than silently degrading the default path.

Extract and publish rather than keep internal

The library ships as two npm packages — doclens and pdfnova — with docs and demos. Ship the library, not the abstraction: internal utilities that never leave the repo are missed opportunities.

Trade-off: Publishing means versioning discipline, issue triage, and documentation for strangers. The forcing function made the API dramatically better than an internal-only tool would have been.

The outcome

Two published npm packages used internally at Interface.ai for all document rendering: 14+ formats, full-text search with OCR, dark mode, multi-document tabs — with a tree-shakeable core of roughly 6 KB.

The live demo on this site renders my actual resume PDF in the browser through pdfnova's WASM engine — the proof is one click away.

Formats supported

Before
1 (PDF only)
After
14+

Core bundle size

Before
n/a
After
~6 KB

What I'd do differently

I'd design the worker/threading story before the first adapter, not after — retrofitting off-main-thread rendering into adapters that assumed the main thread was the most painful refactor of the project.

I'd also publish smaller, earlier. I sat on the internal version for months polishing; the feedback from real external users improved the API faster than any of that polish did.