# 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()`](https://kernstra.com/docs/web-sdk/api/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](https://kernstra.com/docs/web-sdk/guides/worker) for wiring it. Any bundler that reads `package.json` `exports` builds the SDK as is — the Angular CLI, esbuild and Rollup included.

## Example

```ts
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](https://kernstra.com/docs/web-sdk/guides/worker)) |
| 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](https://kernstra.com/docs/web-sdk/guides/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`](https://kernstra.com/docs/web-sdk/api/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](https://kernstra.com/docs/web-sdk/guides/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](https://kernstra.com/docs/web-sdk/config/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](https://kernstra.com/docs/web-sdk/guides/installation).

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()`](https://kernstra.com/docs/web-sdk/api/kernstra/isinitialized) returns `true` and [`Kernstra.version`](https://kernstra.com/docs/web-sdk/api/kernstra/version) matches the installed package.


Web page: https://kernstra.com/docs/web-sdk/guides/engine-assets
