File Viewer began with a consumer-facing application whose backend resources were limited. In 2022, my manager asked me to move document preview entirely into the frontend. That constraint eventually grew into an Apache-2.0 viewer ecosystem, and Worker/WASM delivery became one of its hardest practical deployment problems.
The hardest part has been making many format engines behave consistently while keeping large customer files usable. I spent a lot of time on Worker loading, asset paths, and performance so heavy parsers remain practical in real deployments.
File Viewer previewing an English DOCX sample in the browser. Renderer chunks, Workers, WASM modules, and fonts can all be served from infrastructure you control.
A browser-side file viewer is often selected for privacy: a document can be parsed and rendered without first being uploaded to a third-party conversion service.
That is only half of the deployment story.
A modern viewer may still load renderer chunks, Web Workers, WASM modules, fonts, and vendor assets at runtime. If those resources come from a public CDN, the deployment is not truly self-contained. If their URLs are not planned in advance, a strict Content Security Policy can also turn a successful local integration into a production failure.
This article describes the model I use for self-hosting File Viewer, an Apache-2.0 browser-native viewer for internal and private web applications. The same principles apply to many JavaScript applications that combine lazy imports, Workers, WASM, and private file URLs.
The examples use v2.2.2. Exact asset filenames can change between releases, so pin a version and use the asset manifest produced by that release instead of copying filenames from an old deployment.
Start with the runtime graph, not the component tag
The visible component is only the first node in the runtime graph.
A complete deployment may include:
The host application and framework wrapper.
A core viewer package.
Renderer bundles loaded only when a matching file is opened.
Worker scripts for pipelines such as PDF, DOCX, PPTX, spreadsheets, or archives.
WASM modules used by archive, CAD, and Typst-related paths.
Fonts and other vendor assets.
The source file being previewed.
This distinction matters because loading the initial JavaScript successfully does not prove that the deployment works. A PDF may open while a DOCX Worker is blocked. A ZIP file may fall back to a limited compatibility path because its Worker found the wrong WASM URL. A Typst document may fail only after the compiler module or font assets are requested.
File Viewer currently maps 206 extensions to 24 preview pipelines. That is a routing and capability matrix, not a claim that all 206 extensions use the same renderer or provide identical fidelity.
For deployment testing, think in pipelines rather than extensions.
Prefer one controlled origin
The simplest strict-CSP arrangement is:
https://app.example.internal/
app JavaScript and CSS
/static/file-viewer/v2.2.2/
/api/files/...
With the application, viewer assets, and file endpoint on the same origin, most runtime requests can remain under 'self'.
A separate private static origin also works:
https://app.example.internal/
https://assets.example.internal/file-viewer/v2.2.2/
https://files.example.internal/...
However, this requires three configurations to agree:
The viewer must generate the correct asset URLs.
CSP must allow the asset and file origins under the appropriate directives.
The static and file servers must return suitable CORS and content-type headers.
Same-origin hosting removes an entire class of deployment errors, so I use it unless a private CDN is operationally necessary.
Pin and copy the whole asset layout
There are several ways to integrate the viewer, and their asset behavior differs slightly.
The @file-viewer/web-full IIFE loads the Custom Element and controller first, then resolves heavy renderer bundles and runtime assets relative to the script URL. For this mode, mirror the complete dist directory and preserve its structure. Copying only the entry JavaScript is not enough.
For Vue full packages, the copied assets normally use /file-viewer/ as their default base. A versioned private path can be set explicitly:
import { setDefaultFullAssetBaseUrl } from '@file-viewer/vue3-full'
setDefaultFullAssetBaseUrl('/static/file-viewer/v2.2.2/')
The asset-copy output includes a generated flyfish-viewer-assets.json. Keep the copied directory intact and treat the manifest as the release’s source of truth.
If the application uses composed packages, a Vite project can use @file-viewer/vite-plugin to discover installed presets and copy runtime assets. Other build systems can publish the same assets through their own copy step.
The important principle is the same in every case: do not let a bundler rename one Worker, flatten a WASM directory, and leave the application guessing where the files moved.
Use explicit URLs when the gateway changes paths
A base URL is convenient, but explicit options should remain available for reverse proxies, tenant prefixes, or unusual static gateways.
For example:
const viewerOptions = {
archive: {
workerUrl:
'/static/file-viewer/v2.2.2/vendor/libarchive/worker-bundle.js',
wasmUrl:
'/static/file-viewer/v2.2.2/vendor/libarchive/libarchive.wasm'
},
docx: {
worker: true,
workerUrl:
'/static/file-viewer/v2.2.2/vendor/docx/docx.worker.js',
workerJsZipUrl:
'/static/file-viewer/v2.2.2/vendor/docx/jszip.min.js'
},
spreadsheet: {
worker: false
}
}
Those archive and DOCX locations reflect the documented copied layout. For PDF, Typst, CAD, and other pipelines, take the precise versioned filenames from the copied assets or generated manifest rather than inventing a path.
The spreadsheet Worker is optional and disabled by default. Main-thread parsing avoids Worker startup problems in local servers, restrictive WebViews, and CSP-sensitive environments. That is a useful reminder: a Worker is an architectural choice, not automatically a requirement for every parser.
Build CSP from observed resource types
Start with Content-Security-Policy-Report-Only, exercise the required pipelines, and inspect every violation before enforcing the policy.
A same-origin starting point may look like this:
Content-Security-Policy-Report-Only:
default-src 'none';
base-uri 'none';
object-src 'none';
frame-ancestors 'self';
script-src 'self' 'wasm-unsafe-eval';
worker-src 'self';
connect-src 'self';
img-src 'self' data: blob:;
style-src 'self' 'unsafe-inline';
font-src 'self' data:;
media-src 'self' blob:;
frame-src 'self';
This is a starting template, not a universal policy.
Several directives deserve special attention:
worker-src 'self'permits statically hosted same-origin Workers. Avoid addingblob:automatically. Add it only if an enabled and reviewed code path actually creates blob Workers.script-src 'wasm-unsafe-eval'permits WebAssembly compilation without granting general JavaScript string evaluation in browsers that support this CSP source expression. Test the exact browser fleet before deciding whether a broader exception is acceptable.connect-srcmust include every origin used to fetch documents, renderer data, WASM, or signed file URLs.img-src,font-src, andmedia-srcdepend on the document types you enable. Local uploads and generated previews can requireblob:ordata:.style-src 'unsafe-inline'may be needed by document layout surfaces that produce runtime styles. If the host already uses a stricter nonce- or hash-based design, validate each renderer instead of weakening the policy globally.frame-srcis only necessary when using the iframe distribution or another framed preview surface.
For a private asset origin, replace the relevant 'self' entries with the exact origin. Avoid wildcards such as https: because they erase much of the value of CSP.
WASM failures are not always WASM failures
When a WASM pipeline fails, first inspect the network response.
A common failure sequence is:
The application requests
libarchive.wasm.The path is missing.
An SPA fallback returns
index.htmlwith status 200.The browser reports a WASM compilation or MIME error.
The underlying problem is routing, not the binary.
Static asset routes should fail with a real 404 instead of falling through to the application shell. Also verify these response types:
.js / .mjs JavaScript MIME type
.wasm application/wasm
fonts matching font MIME type
Use X-Content-Type-Options: nosniff after the MIME mapping is correct, not before it has been tested.
The archive Worker and libarchive.wasm should remain adjacent when using the default layout, unless both URLs are explicitly configured. The DOCX Worker also needs access to its JSZip dependency. A successful Worker request alone does not prove that its secondary imports succeeded.
Cache releases, not mutable filenames
A versioned directory makes CSP deployment and rollback much easier:
/static/file-viewer/v2.2.2/...
/static/file-viewer/v2.2.2/...
Immutable versioned JavaScript, Workers, WASM, and fonts can receive long cache lifetimes. HTML entry points and any manifest used to select the current release should have shorter caching.
This avoids a dangerous mixed-release state where the application loads a new renderer with an old Worker or WASM module from the browser cache.
It also allows a rollback to change one application reference instead of overwriting assets in place.
Treat iframe mode as a separate security boundary
The release provides a zero-dependency iframe entry for hosts that do not want an npm integration.
If the iframe and parent are on different controlled origins, remember that they are separate documents with separate CSP policies. Restrict frame-ancestors on the viewer origin, restrict frame-src on the parent, and use an exact target origin for postMessage.
Do not use '*' as the target origin for document blobs.
If the parent fetches the private file and sends a Blob into the iframe, the file authorization remains in the parent application. The iframe origin still needs permission to load its own Workers, WASM, fonts, and renderer chunks.
Test one file from every expensive pipeline
My minimum private-deployment matrix is not 206 sample files. It is a smaller set selected to activate different runtime paths:
PDF for PDF.js Worker and Range behavior.
DOCX for the document Worker and JSZip asset.
PPTX for progressive slide processing.
ZIP or 7z for the archive Worker and WASM pair.
DWG for the CAD Worker and LibreDWG WASM path.
Typst for compiler WASM, renderer WASM, and fonts.
A local image or media file for blob URLs.
The iframe entry, if the application uses it.
Then I verify:
No request reaches an unapproved public origin.
Every Worker runs under the enforced CSP.
WASM responses have the correct MIME type.
Missing assets return 404 rather than HTML.
Version rollback does not mix cached releases.
The same tests work in required mobile or embedded WebViews.
CSP reports are empty after expected user actions, not merely after page load.
The boundary of “browser-native”
Browser-native does not mean “no network requests.” The browser still needs to fetch the source file and the assets required for its pipeline. The privacy property comes from controlling those endpoints and avoiding a mandatory document-conversion service.
It also does not mean every extension has identical fidelity. Some pipelines render rich visual content, some progressively parse legacy files, and some provide safe structural inspection. Heavy CAD/BIM formats may still be better served by a deliberate conversion workflow. PlantUML rendering uses a configurable endpoint, which must be self-hosted or excluded if the environment cannot make that request.
The project is a viewer, not a document editor. Download preserves the original bytes; it does not write the rendered result back into the source document. Print and HTML export are exposed only where the corresponding renderer can provide a meaningful result.
Those boundaries are part of a secure design because they prevent a broad support matrix from being mistaken for a universal document engine.
Final thought
A strict CSP should not be the final obstacle added after integration. It should shape the asset model from the beginning.
Once Workers, WASM, fonts, renderer chunks, and source files have stable versioned locations, the policy becomes understandable. More importantly, the private-deployment promise becomes verifiable: open the network panel, exercise each pipeline, and confirm exactly where every byte came from.
GitHub: https://github.com/flyfish-dev/file-viewer
Distribution guide: https://doc.file-viewer.app/en/guide/distribution
English demo: https://demo.file-viewer.app/?lang=en-US

Top comments (0)