# A local-first PDF toolkit built on Nuxt and OPFS

> A dev.to report describes beePDF, a client-side PWA that keeps PDF processing in the browser, with OPFS holding the file bytes and IndexedDB the metadata.

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](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3)).

## 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](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3)).

### 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](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3)).

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()](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/getDirectory)).

### 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](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3)).

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](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system)).

## 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](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3)).

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](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3)).

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](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/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](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle)).

### 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](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3)).

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](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)).

### 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](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3)).

## 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](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/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](https://developer.mozilla.org/en-US/docs/Web/API/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](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system)).

## Related entries on this site

- [Storage](/reference/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](/reference/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](/reference/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

- 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 `FileSystemSyncAccessHandle` to 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

- [Why I built a local-first PDF toolkit using Nuxt and OPFS](https://dev.to/eoinmcg/why-i-built-a-local-first-pdf-toolkit-using-nuxt-and-opfs-54k3) (dev.to)
- [Origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) (developer.mozilla.org)
- [FileSystemSyncAccessHandle](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle) (developer.mozilla.org)
- [StorageManager: getDirectory() method](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/getDirectory) (developer.mozilla.org)
- [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) (developer.mozilla.org)