A local-first PDF toolkit built on Nuxt and OPFS
Published
On 2026-10-06 the dev.to account eoinmcg published a build report on beePDF, a PDF tool the report describes as a 100% client-side Progressive Web App where document processing happens entirely in the browser and documents are never uploaded to a remote server. The report sets out the architecture behind that claim: the origin private file system holds the document bytes, IndexedDB holds metadata and thumbnails, and the processing work runs in Web Workers (the report).
The problem the report starts from
Section titled “The problem the report starts from”The report’s starting position is about trust rather than throughput. It states that mainstream tools, naming Smallpdf and ILovePDF, ask the user to upload the sensitive document to their remote server, and that even when services use encryption in transit and promise to delete files after a limited period, uploading confidential documents to a remote server still means entrusting those documents to a third-party service. Its own summary of the distinction is that encryption in transit protects the transfer and does not eliminate the need to trust the system that receives and processes the document. It adds that depending on the type of files and the use case, giving legal and medical as its examples, uploading them to a third-party service may not be an option due to privacy, confidentiality, or regulatory requirements (the report).
The stack the report lists
Section titled “The stack the report lists”The report gives its stack as a list. The framework is Nuxt, which it annotates
Vue 4, TypeScript, Vite. PWA and offline support come from @vite-pwa/nuxt plus
Service Workers. In-memory file processing uses pdf-lib, PDF.js, and the
native Canvas API. Local persistence is the origin private file system, which the
report reaches through navigator.storage.getDirectory(). Styling and UI are
Pico CSS
(the report).
MDN documents the entry point that list names. It describes
StorageManager.getDirectory() as the method used to obtain a reference to a
FileSystemDirectoryHandle object allowing access to a directory and its
contents, stored in the origin private file system, and states that it returns a
promise fulfilling with that handle. MDN also documents two ways the call can
fail: a SecurityError thrown if the browser is not able to map the requested
directory to the local OPFS, for example due to storage or memory constraints,
and which some browsers also throw if getDirectory() is called in private
browsing mode, and an UnknownError thrown in some browsers in private browsing
mode
(MDN, StorageManager.getDirectory()).
Why the report rejects the other storage options
Section titled “Why the report rejects the other storage options”The report rules out the alternatives in turn. It states that LocalStorage is limited to roughly 5MB to 10MB and is synchronous text-only storage, which it calls completely useless for binary PDFs. It states that IndexedDB is excellent for structured metadata and smaller binary objects, but less natural for treating large documents as persistent files with efficient byte-level access. It states that the File System Access API is great when an app needs to work directly with files in the user’s filesystem, but that access requires user permission and is subject to browser security restrictions (the report).
MDN’s own account of why the origin private file system exists tracks that third objection closely. MDN describes the File System Access API as providing access to files using picker methods, and states that because those changes are being made to the user-visible file system there are a lot of security checks in place, giving safe browsing in Chrome as its example, to guard against malicious content being written to that file system. MDN states that those writes are not in-place and instead use a temporary file, that the original is not modified unless it passes all the security checks, and that as a result these operations are fairly slow, with the performance suffering when making more significant, large-scale file updates such as SQLite database modifications (MDN, Origin private file system).
What the report says OPFS gives it
Section titled “What the report says OPFS gives it”The report describes the origin private file system as providing a private,
origin-scoped filesystem designed for persistent file-like data, with low-level
byte access and optimized in-place writes, and states that it also offers
synchronous access handles inside Web Workers for performance-sensitive
workloads. Its code sample obtains the root with
await navigator.storage.getDirectory(), takes a file handle with
root.getFileHandle("working-draft.pdf", { create: true }), and writes a binary
buffer through createWritable(), write(), and close()
(the report).
The report then names the properties it gives as the reasons OPFS matters for
PDF processing. First, zero permission prompts: the report states that because the directory is
private to https://beepdf.app, the browser does not spam the user with
permission modals. Second, disk input and output speed: the report states that
OPFS provides direct byte-level file streams, and that in Web Workers
createSyncAccessHandle() can be used for near-native, synchronous file read and
write operations. Third, quotas and cleanup: the report states that files persist
across browser reloads, so a working document survives accidentally closing the
tab, but that they automatically honor the browser’s origin storage quota
(the report).
MDN backs each of those properties with its own wording. On permissions,
MDN states that permission prompts and security checks are not required to access
files in the OPFS, and that the OPFS is private to the origin of the page and not
visible to the user like the regular file system. On speed, MDN states that the
OPFS offers low-level, byte-by-byte file access, that it does not require the
same series of security checks and permission grants and is therefore faster than
File System Access API calls, and that it has a set of synchronous calls
available, where other File System API calls are asynchronous, that can be run
inside web workers only so as not to block the main thread. On quotas, MDN states
that the OPFS is subject to browser storage quota restrictions, just like any
other origin-partitioned storage mechanism, that the amount of space it uses can
be read through navigator.storage.estimate(), and that clearing storage data
for the site deletes the OPFS
(MDN, Origin private file system).
MDN adds a constraint on the synchronous handle that the report’s wording leaves
implicit. MDN states that FileSystemSyncAccessHandle is only accessible inside
dedicated Web Workers, so that its methods do not block execution on the main
thread, and only for files within the origin private file system, and that as a
result its methods are not subject to the same security checks as methods
running on files within the user-visible file system and so are much more
performant. MDN also notes that despite having “Sync” in its name,
createSyncAccessHandle() is itself asynchronous, and that close() releases
the exclusive lock previously put on the file associated with the handle
(MDN, FileSystemSyncAccessHandle).
The pipeline the report describes
Section titled “The pipeline the report describes”The report walks a dropped PDF through the stages it names. Local intake: the
browser reads the File object into an ArrayBuffer in memory. OPFS: the document is
immediately stored in the local OPFS workspace directory. Metadata and a very
small thumbnail are stored in IndexedDB, which the report says also makes
searching and tagging trivial. Web Worker offloading: tasks the report names as
rendering page thumbnails, extracting plain text or Markdown, and running
word-level diffs are offloaded to Web Workers to keep the UI running at 60 FPS.
Client-side export: modifying or signing a document creates a fresh Uint8Array
in memory, which triggers a direct client download via a URL.createObjectURL
Blob. The report also describes an airplane mode test, stating that beePDF can be
loaded, the connection turned off completely, and documents merged, split,
compressed, or signed offline
(the report).
MDN’s description of the role the report assigns to IndexedDB matches the split it describes. MDN describes IndexedDB as a low-level API for client-side storage of significant amounts of structured data, including files and blobs, states that it uses indexes to enable high-performance searches of that data, and says that while Web Storage is useful for storing smaller amounts of data it is less useful for storing larger amounts of structured data. MDN states that operations performed using IndexedDB are done asynchronously so as not to block applications, and that IndexedDB is available in web workers (MDN, IndexedDB API).
The limits the report reports
Section titled “The limits the report reports”The report names its limits. It states that processing massive 500MB+ scanned PDFs inside browser JS can hit V8 heap memory limits on lower-end devices. It states that heavy image compression or canvas manipulation must be offloaded to Web Workers, because otherwise rendering high-DPI canvases will freeze the UI thread. Its framing of both is that modern browsers provide a rich set of capabilities reachable through standardized Web APIs, and that building pure client-side file tools is nevertheless not without its limits (the report).
Why it matters for PWA developers
Section titled “Why it matters for PWA developers”The report’s architecture turns a privacy requirement into a storage decision, and the decision is made against properties the platform documentation states rather than against a framework preference. The rejection of the File System Access API is not about ergonomics: MDN states that its writes go through security checks against the user-visible file system, use a temporary file rather than writing in place, and are therefore fairly slow for large-scale updates. Those are the same properties the OPFS inverts, which MDN summarizes as byte-by-byte access, no permission prompts or security checks, and synchronous calls restricted to web workers (MDN, Origin private file system).
The split the report draws between the two client-side stores is the reusable part. Document bytes go to the OPFS, where MDN documents byte-level reads and writes and a synchronous handle usable in dedicated workers; metadata and thumbnails go to IndexedDB, which MDN documents as indexed storage of structured data with high-performance searches over it. An app that needs both large opaque blobs and queryable records about them has two different tools documented for the two different jobs, and the report assigns each the job its documentation describes (MDN, IndexedDB API).
Persistence is the part that needs care. MDN states that the OPFS is subject to browser storage quota restrictions like any other origin-partitioned storage mechanism, and that clearing storage data for the site deletes the OPFS. MDN also states that browsers persist the contents of the OPFS to disk somewhere, but that the created files cannot be expected to be found matched one-to-one, because the OPFS is not intended to be visible to the user (MDN, Origin private file system).
Related entries on this site
Section titled “Related entries on this site”- Storage: the report’s central decision is which client-side store holds the document bytes, and this page is where OpenPWA covers the origin-scoped storage mechanisms, their quotas, and their eviction behaviour.
- Capabilities: the origin private file system and the synchronous access handle MDN restricts to dedicated workers are platform capabilities with their own support story, and this page is where OpenPWA tracks what a PWA can reach for.
- Performance: the report’s stated target is keeping the UI at 60 FPS by moving thumbnail rendering, text extraction, and diffs off the main thread, and this page is where that budget is documented.
Open and unverified
Section titled “Open and unverified”- Which browsers and versions beePDF was tested on is not stated by the report, so the claims about speed and the airplane mode test cannot be pinned to an engine.
- Whether the report’s synchronous access handles run in dedicated workers is not
stated; MDN restricts
FileSystemSyncAccessHandleto dedicated Web Workers. - Whether beePDF requests persistent storage, and so what happens to an OPFS working document under quota pressure, is not addressed by the report.
- At what file size the V8 heap limits the report names are reached, and on which devices, is not stated beyond the report’s “500MB+ scanned PDFs” and “lower-end devices”.
- Whether the roughly 5MB to 10MB LocalStorage figure the report gives for comparison holds on any particular browser is not confirmed.
- Whether private browsing mode, in which MDN states some browsers throw on
getDirectory(), was tested is not addressed.
Sources
Section titled “Sources”- Why I built a local-first PDF toolkit using Nuxt and OPFS (dev.to)
- Origin private file system (developer.mozilla.org)
- FileSystemSyncAccessHandle (developer.mozilla.org)
- StorageManager: getDirectory() method (developer.mozilla.org)
- IndexedDB API (developer.mozilla.org)