Skip to content
ComPDF
New Release

Open-Source PDF SDK & AI Document Processing

Get the full self-hosted SDK and AI document processing on GitHub. One-click deploy to quickly build your document workflows.

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)
LayerLocationStateOpen source?
Embed entrypackages/core/webviewer.jsCreates the iframeYes (thin)
UI layerpackages/webviewPinia storesYes
Core bridgepackages/webview/src/core/Stateless (one registry)Yes
Enginepackages/coreWASM-backedNo

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 onto window.instance.UI (and the engine surface onto window.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.js applies both plugins to one app: setupStore(app) (Pinia) and i18n(app) (i18next-vue) are applied to the same createApp(App) instance, which is then mounted. (An earlier version created a second createApp(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:

javascript
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, documentStore via api_helpers/createEncapsulatedStore.js): exposes only dispatch(action)(args) and getState(key) (returns a deep clone). Used by the popup APIs, getPassword, and getPanels.

Multi-instance model

The bridge supports multiple simultaneous PDF viewers via a single Map in src/core/documentViewers.js:

javascript
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 docNum parameter 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 import packages/core directly; they go through src/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; docNum is 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 document store is the UI-side mirror of engine tool/style state — setters like setActiveToolColor and setToolState write to the store and push into core.
  • Encapsulated store seam: dispatch/getState on 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:

  • circle without measure maps to square.
  • ink with measure maps to curve.
  • ink with arc points maps to arc.

These aliases matter when working with tools and annotation styles.

Where to go next