Architecture
ComPDFKit Web SDK follows a strict three-layer architecture. Understanding the boundaries between these layers is essential for customizing the UI and extending the SDK.
The three layers
Integrator's page
└─ webviewer.js (iframe embed — packages/core/webviewer.js)
└─ Webview Vue app (packages/webview — OPEN SOURCE)
└─ Core bridge (packages/webview/src/core/)
└─ ComPDFKitViewer (packages/core — CLOSED SOURCE)
└─ Web Worker (compdfkit_worker.js)
└─ WASM (ComPDFKit.wasm / pdfium.wasm)| Layer | Location | State | Open source? |
|---|---|---|---|
| Embed entry | packages/core/webviewer.js | Creates the iframe | Yes (thin) |
| UI layer | packages/webview | Pinia stores | Yes |
| Core bridge | packages/webview/src/core/ | Stateless (one registry) | Yes |
| Engine | packages/core | WASM-backed | No |
1. Embed entry — webviewer.js
The integrator calls ComPDFKitViewer.init(options, element). This creates an <iframe> pointing at the built webview app, performs a config handshake with the iframe, and resolves with the instance object ({ UI, Core, docViewer }) captured from iframe.contentWindow.instance. See Getting Started.
2. UI layer — packages/webview
A Vue 3 + Pinia SPA. It owns all UI state (which panels are open, the active tool, theme, toolbar definition) and renders the entire interface. It never imports packages/core directly — it talks to the engine exclusively through the core bridge.
Subsystems:
src/apis/— the public API surface assembled ontowindow.instance.UI(and the engine surface ontowindow.instance.Core).src/core/— the stateless bridge.src/components/— ~200+ Vue components.src/stores/— Pinia stores (viewer,document).src/helpers/,src/hooks/,src/constants/.
3. Core bridge — src/core/
A flat facade of ~120 single-function modules that forward calls to the ComPDFKitViewer engine instance. It holds no business state and performs no DOM operations — its only mutable structure is the documentViewers Map (the multi-instance registry). See Core Bridge.
4. Engine — packages/core
The ComPDFKitViewer class (src/index.js, ~5400 lines) is the monolithic heart of the SDK. It manages PDF rendering, annotations, forms, content editing, and measurement, and communicates with the WASM worker via a MessageHandler. It builds via Rollup into packages/webview/lib/webview.min.js (main library) and lib/PDFWorker.js (worker), which the webview imports at build time.
Bootstrap flow
The end-to-end load sequence:
1. Integrator: ComPDFKitViewer.init(options, el)
2. webviewer.js: create iframe → webviewer/index.html?version=...#d=...&docId=...
3. iframe boots: main.js → createApp(App= Suspenser.vue) → setupStore + i18n → mount('#app')
4. Suspenser.vue renders <App/> inside <Suspense> (fallback: loading logo)
5. App/index.vue setup():
await i18nextPromise // translations ready
initDocument({ $t }) // new ComPDFKitViewer(options) from lib/webview.min.js
// → core.setDocumentViewer(1, documentViewer)
defineWebViewerInstanceUIAPIs() // window.instance = { docViewer, Core, UI }
await loadConfig(initOptions) // iframe↔parent handshake, apply integrator options
read hash params: css / theme / header
6. App onMounted: postMessage({ type: 'viewerLoaded', docId }) → window.parent
7. webviewer.js: capture iframe.contentWindow.instance, fire 'ready', resolve init promise
main.jsapplies both plugins to one app:setupStore(app)(Pinia) andi18n(app)(i18next-vue) are applied to the samecreateApp(App)instance, which is then mounted. (An earlier version created a secondcreateApp(App)for i18n and mounted that instead, leaving the Pinia-enabled app unmounted — this has been fixed.)
The instance object
Defined in packages/webview/src/apis/index.js:
window.instance = {
docViewer, // = core.getDocumentViewer() (instance 1)
Core: objForWebViewerCore, // spread of the entire core bridge
UI: objForWebViewerUI, // all apis/*.js factories, store-injected
};objForWebViewerUI is built from apis/ modules. Each module is a factory (stores...) => (...args) => result; index.js injects the Pinia stores at registration time, so the public surface is the inner function. Two store-access patterns coexist:
- Direct Pinia store (
useViewer,useDocument): the API calls store actions/getters directly. Most APIs use this. - Encapsulated store (
viewerStore,documentStoreviaapi_helpers/createEncapsulatedStore.js): exposes onlydispatch(action)(args)andgetState(key)(returns a deep clone). Used by the popup APIs,getPassword, andgetPanels.
Multi-instance model
The bridge supports multiple simultaneous PDF viewers via a single Map in src/core/documentViewers.js:
const documentViewerMap = new Map();
export const getDocumentViewer = (number = 1) => documentViewerMap.get(number);
export const setDocumentViewer = (number, documentViewer, options) => { /* … */ };
export const deleteDocumentViewer = (number) => { documentViewerMap.delete(number); };- Default instance is
1— the primary document.getDocumentViewer()with no argument resolves to instance 1. - Instances 2 and 3 are used by the document-compare feature (the "old file" and "new file" viewers), registered by
CompareDocumentContainer.vue. - Only ~13 of the ~120 bridge files plumb an explicit
docNumparameter through; the rest are hardwired to instance 1. Multi-instance is therefore opt-in per method, not uniform across the API surface.
Key design patterns
- One-way dependency rule:
apis/ → core/ → engine. APIs never importpackages/coredirectly; they go throughsrc/core/. The bridge never touches Pinia stores or the DOM. - Stateless bridge:
src/core/files contain no business logic — they are pure delegation functions. One function per file; filename === function name;docNumis always the last parameter. - One function per file (apis): Each
apis/module exports one factory, verb-prefixed (set/get/open/disable). Includes JSDoc. - EventBus: The engine uses a custom EventBus (
core.eventBus()) for cross-module communication —annotationChange,toolChanged,scalechanging,pagerendered, etc. See Events. - Message queue:
ComPDFKitViewer.enqueue()serializes async operations to prevent race conditions on the WASM worker. - Store mirrors engine: The Pinia
documentstore is the UI-side mirror of engine tool/style state — setters likesetActiveToolColorandsetToolStatewrite to the store and push intocore. - Encapsulated store seam:
dispatch/getStateon both stores are explicitly the API-access channel, keeping the public surface decoupled from Pinia internals.
Annotation type aliases
Some annotation types share implementation in the engine:
circlewithout measure maps tosquare.inkwith measure maps tocurve.inkwith arc points maps toarc.
These aliases matter when working with tools and annotation styles.
Where to go next
- Core Bridge — the
src/core/facade in depth. - State Management — the Pinia stores.
- Components — the Vue component tree.
- API Reference — the full public surface.