Engine, binaries and fonts
There is nothing to set up. The SDK's engine, pdf-engine.bin (~4.4 MB), is not in this package: Kernstra.init() downloads it from the asset store your license names, verifies it against the digest built into the SDK, and keeps the verified copy in the browser's Cache Storage so the next load downloads nothing. The same goes for the text-shaping binary, the font-subsetting binary and every bundled font. The minimal, and usual, initialization is await Kernstra.init({ license: import.meta.env.KERNSTRA_LICENSE });.
Nothing to copy, no asset paths, no bundler configuration, and no runtime dependency: the package's only file beside the JavaScript is the render worker, which is SDK code; see Web worker rendering for wiring it. Any bundler that reads package.json exports builds the SDK as is — the Angular CLI, esbuild and Rollup included.
Example
import { Kernstra, PDFViewer } from '@kernstra/pdf-web-sdk'; import RenderWorkerUrl from '@kernstra/pdf-web-sdk/render-worker?url'; // An offline (air-gapped) license: everything comes from your own servers. await Kernstra.init({ license: import.meta.env.KERNSTRA_LICENSE, // the offline token engineUrl: '/assets/pdf-engine.bin', shaperUrl: '/assets/pdf-shaper.bin', subsetterUrl: '/assets/pdf-subset.bin', workerUrl: RenderWorkerUrl, }); const viewer = new PDFViewer(container, doc, { fonts: { fontsBaseUrl: '/assets/fonts/' }, // or { download: false } });
What leaves the browser
| Request | To | Carries | When |
|---|---|---|---|
| Licensing status check | The licensing API the signed token names | The token, a random nonce, the time, a pseudonymous identifier from a key created once per browser (stable, of this browser on your origin — name it in your privacy notice), and a small record about the SDK: its version and build, how the load went (whether it is init or setLicense, where the token came from, failure counts, how long ago the last answer was signed, the last error code) and whether the browser could keep its key | Kernstra.init(), setLicense(), and a document open when the last signed answer is older than an hour; never for an offline token; never on a timer |
| Asset download | The asset store the token names | Nothing; the URL is the file's kind (engine, shaper, subsetter, fonts) and its digest (the engine, the shaping binary, the subsetting binary, a bundled font) | First load on a browser (the engine); first use of a text tool (the shaper, the subsetter, a substitute font). A font request reveals, by digest, which substitute a document needed; fonts.download: false or self-hosting removes that |
| Self-hosted assets | Your own URLs (engineUrl, shaperUrl, subsetterUrl, fonts.fontsBaseUrl, fonts.customFonts[].ttfUrl) | Nothing | The same moments |
| The render worker | Your own origin (workerUrl) | The document's bytes, to render them | When workerUrl is set: viewing runs on it under every license; after an edit it keeps running only under a production license that includes export with clean output (see Web worker rendering) |
| Google Fonts | fonts.googleapis.com | Nothing (the browser's request) | Only with tools.sign.webFonts: true; off by default |
| Your own calls | Where you point them | What you pass | PDFDocument.fromUrl(), uploadTo(), tools.stamp.custom[].url |
| A link in the document | The link's address | Nothing | Only when the user clicks it and confirms (or your onExternalLink decides); http, https, mailto and tel only |
What is stored
| Store | Holds | Lifetime |
|---|---|---|
localStorage kernstra-license-token, -update, -receipt, -retry-after, -diag | The presented token, a token adopted from a signed update, the last signed status answer, a back-off deadline, failure counters | Until setLicense(''), a revocation, or the site data is cleared; Kernstra.destroy() keeps them, so the next load does not wait |
IndexedDB kernstra-license | A key created once per browser: its pseudonymous identifier for the status check | Until the site data is cleared |
Cache Storage kernstra-assets | The verified engine, shaping and subsetting binaries and bundled fonts, by store URL; checked again on every read | Until the site data is cleared |
| Memory only | Saved signatures (at most 3), the user's own stamps (at most 8), recent images, recent colors | The page's lifetime; several viewers on one page share them (see What you can replace); persisting them is yours through the events |
Notes
Integrity. Every download is verified. The SDK holds the SHA-256 of each file its version runs, asks the store for that digest, and checks the bytes it gets, from the store or from the cache. An engine binary that is not this version's fails with ENGINE_LOAD_FAILED and never executes; a shaping or subsetting binary or a font that fails the check is never used. The digests are exposed as Kernstra.assetDigests ({ engine, shaper, subset, fonts }, fonts keyed by font id) for scripting a self-hosted deployment.
Availability. The first load on a browser needs the asset store; every later load is served from the cache. A browser that holds a recent signed answer from the licensing API never waits for it: the status check runs in the background. Only a load with no such answer waits for the check, at most 8 seconds: the first on a browser, after site data was cleared, in a frame without storage, with an answer older than 48 hours, under a paused license, or on a host the license does not name (see Licensing). If the store cannot be reached on a browser that has never loaded before, init() rejects with ENGINE_LOAD_FAILED and the message names the host it tried. A self-hosted copy is not kept in Cache Storage; it is fetched from your URL on each load, so serve it with your own HTTP caching headers.
What leaves the browser, and what is stored. For a security questionnaire. Nothing opened — document bytes, text, field values, file names, passwords — is ever sent or stored by the SDK; the requests in the table above carry no document-derived field. No beacon, no telemetry, no crash reporting, no other endpoint. No sessionStorage, no cookies. Every store is the page origin's; two tenants on one origin share them.
Self-hosting (licensed). A license that includes self-hosting may serve the binaries and the bundled fonts from its own infrastructure. Today that is an offline license (see below): online licenses download them from the asset store the license names. Fetch each file once from the store — the paths are the digests under a folder per kind (engine, shaper, subsetter, fonts), and Kernstra.assetDigests names them (engine, shaper, subset — the subsetter folder — and fonts[id]) — with curl "$STORE/engine/<engine digest>" -o public/assets/pdf-engine.bin (and likewise shaper to pdf-shaper.bin, subsetter to pdf-subset.bin), then pass where you put them: await Kernstra.init({ license: import.meta.env.KERNSTRA_LICENSE, engineUrl: '/assets/pdf-engine.bin', shaperUrl: '/assets/pdf-shaper.bin', subsetterUrl: '/assets/pdf-subset.bin' });.
The bundled fonts are fetched the same way, once each, by Kernstra.assetDigests.fonts[font.id], and served under fonts.fontsBaseUrl by each font's ttfPath: a deployment script loops over BUILTIN_FONTS.filter((f) => f.source === 'bundled') and, for each, fetches $STORE/fonts/<Kernstra.assetDigests.fonts[font.id]> into public/assets/fonts/<font.ttfPath>; the viewer then takes new PDFViewer(container, doc, { fonts: { fontsBaseUrl: '/assets/fonts/' } }). The SDK verifies their bytes exactly as it verifies a download (fonts too, unless you turn that off with fonts.verifyIntegrity: false). Without self-hosting in the license, engineUrl, shaperUrl and subsetterUrl are refused by Kernstra.init() with FEATURE_NOT_LICENSED before anything is fetched, and fonts.fontsBaseUrl is refused the same way when the first text tool opens. Offline (air-gapped) licenses always self-host: they name no store. See Fonts.
Deploying without outbound internet (offline licenses). An offline (air-gapped) license is a token that names no licensing API and no asset store: the SDK never contacts either, and it carries its own expiry, which a renewal replaces with a new token string. Everything the SDK needs then comes from your own servers, and the browser makes no request outside them. The checklist, each line the only place a request could go — the example above wires all of it:
1. The engine, the shaping binary if you use the text tools, and the subsetting binary if you use fonts.subset. Host pdf-engine.bin, pdf-shaper.bin and pdf-subset.bin (fetched once from the store at issue time, or supplied with the license) and pass engineUrl, shaperUrl and subsetterUrl — see Self-hosting above.
2. The bundled fonts. Either host them under fonts.fontsBaseUrl, or set fonts.download: false, which keeps the standard fourteen faces, the fonts embedded in each document and your own customFonts given as bytes, and refuses any other with a descriptive error.
3. The render worker, if you want it. Host render.worker.js and pass workerUrl. The worker takes the engine's bytes from the page and makes no request of its own. Without it the SDK renders on the main thread.
4. The signature dialog's web faces stay off unless you set tools.sign.webFonts: true: leave it unset.
5. A content security policy of connect-src 'self' and worker-src 'self' is enough for requests: the SDK has no telemetry and no other endpoint. The rest of the policy is as in Installation and support.
The "You are offline" strip above the pages follows the browser's own online state, which an intranet host reports as online; where a host's network makes the browser report offline, toolbar.offlineNotice: false keeps the strip away. Nothing else behaves differently: Edit Text, forms, annotations, signatures and export run in the browser whatever the network.
The text-shaping binary. A second, much smaller binary, pdf-shaper.bin (~0.4 MB), is downloaded the same way on the first use of a text tool — Edit Text, Add Text, Text box — which shape the text they write with it: kerning, ligatures and complex scripts (Arabic, Hebrew, Indic). It is verified and cached like the engine. A tool that cannot load it (the store unreachable on a browser that has never used a text tool) falls back to the font's own character map: the text is still written, without kerning, ligatures or complex-script shaping, and the next use tries the download again; a Text box whose words need a bundled face logs a console warning and is drawn with the standard fonts.
The font-subsetting binary. A third binary, pdf-subset.bin (~0.6 MB), is downloaded the same way on first use when fonts.subset is on, so an exported document embeds only the glyphs it uses. It is optional: if it cannot be loaded, the SDK warns once in the console and embeds full fonts, as it does with subsetting off — nothing about editing changes.
Verifying it loaded. After await Kernstra.init({ license }), Kernstra.isInitialized() returns true and Kernstra.version matches the installed package.