---
title: "Getting Started"
description: "Boot a local EmbedPDF Engine, open PDF bytes, and inspect the document."
source: "https://www.embedpdf.com/docs/engine/getting-started"
---

# Getting Started

Install the local engine package:

```sh
pnpm add @embedpdf/engine
```

## Create and use the Engine

`localEngine()` returns the engine itself — construction is synchronous and
cheap, and it allocates nothing (no Worker, no WASM) until the first operation.
The browser implementation runs PDFium in a Web Worker, and the default worker
is bundler-portable (Vite, webpack 5 / Next, Rollup, Parcel), so there is no
worker wiring to write. The example below opens a PDF from fetched bytes, reads
the page list, and cleans up both handles.

**`getting-started.ts`**

```typescript
import { localEngine } from '@embedpdf/engine';

export async function inspectPdf(url: string) {
  // `localEngine()` IS the engine — synchronous, nothing allocated yet. The
  // first operation boots PDFium in a Web Worker; no worker wiring needed
  // (the default worker is bundler-portable).
  const engine = localEngine();

  try {
    const response = await fetch(url);
    if (!response.ok) throw new Error(`Unable to fetch PDF: ${response.status}`);

    const document = await engine.open(
      {
        kind: 'bytes',
        id: 'document',
        bytes: await response.arrayBuffer(),
      },
      { scope: ['*'] },
    );

    try {
      const { pageCount, pages } = await document.pages.list();
      return {
        pageCount,
        firstPageObjectNumber: pages[0]?.pageObjectNumber,
      };
    } finally {
      await document.close();
    }
  } finally {
    // We created the engine, so we destroy it — ownership follows acquisition.
    await engine.destroy();
  }
}
```

Every engine method is async, so nothing changes at the call site whether the
engine has booted or not: the first `open()` (or an explicit `engine.warmup()`)
starts the boot in the background, and pending work simply resolves when PDFium
is ready.

## Who owns the engine

Whoever creates the engine owns it and is responsible for `engine.destroy()`.
With a viewer, ownership follows the shape of what you pass:

- **An instance is borrowed.** `<Viewer engine={engine} />` uses your engine
  and never destroys it. This is the common path — a module-scope
  `const engine = localEngine()` shared across viewers and route changes, alive
  for the lifetime of the app (so you typically never destroy it). The viewer
  calls `engine.warmup()` on mount, overlapping the PDFium boot with the rest
  of app initialization.
- **A thunk is viewer-owned.** `<Viewer engine={() => localEngine()} />` makes
  the viewer create the engine on mount and destroy it on unmount — per-mount
  isolation with automatic cleanup.

## Fallback fonts

Runtime fonts are boot configuration — pass them to `localEngine()` and they
are guaranteed to be registered before any document work runs (font URLs are
fetched in parallel with the worker boot):

```ts
const engine = localEngine({
  fallbackFonts: [{ key: 'noto-cjk', familyName: 'Noto Sans CJK', url: '/fonts/NotoSansCJK.ttf' }],
});
```

`fallbackFonts` are registered *and* added to the glyph-fallback chain (used to
substitute missing glyphs when rendering and generating appearances). Use
`fonts` for fonts you only reference explicitly (e.g. a FreeText `fontFamily`)
without affecting automatic substitution. Give each font either inline `data`
or a `url` fetched at boot.

Fonts can also be registered at any later moment — `engine.fonts` is the live
font service on the local engine:

```ts
const handle = await engine.fonts.register({ key: 'brand', data: bytes });
await engine.fonts.addFallback(handle);
```

## Local vs cloud

`@cloudpdf/engine` exposes a matching `cloudEngine()` with identical ownership
rules, so swapping local for cloud is a one-import change:

```ts
import { cloudEngine } from '@cloudpdf/engine';

const engine = cloudEngine({ baseUrl: 'https://pdf.example.com', token });
```

Two deliberate differences surface the split:

- **What `open()` accepts.** Local opens `{ kind: 'bytes' }`; cloud opens
  `{ kind: 'id' }` / `{ kind: 'token' }`, addressing server-side documents.
- **Fonts.** `cloudEngine()` has no `fallbackFonts` option — fallback fonts are
  a server policy on the cloud (`engine.fonts` is undefined there).

## Server-side rendering (Next, Nuxt, SvelteKit)

An engine constructed at module scope does no work on the server — it allocates
no Worker and touches no WASM until something uses it, which only happens in
the browser (a viewer's mount effect, or your own first `open()` in a client
component). So a module-level `const engine = localEngine()` is SSR-safe. In
the Next.js App Router, mark the component that renders the viewer
`'use client'`.

## Custom workers

For a custom worker setup — a strict CSP, a bundler without `new URL` worker
support, a shared worker — pass your own as a `() => Worker` thunk (called once,
when the engine boots):

```ts
localEngine({ worker: () => new Worker(/* … */) });
```

The raw worker source is also published at `@embedpdf/engine/worker-entry`
(Vite: `@embedpdf/engine/worker-entry?worker`).

## Lifecycle rules

1. One engine serves the documents of one application scope; reuse it.
2. Close each document handle when the document leaves your application.
3. Destroy the engine when you own it and no longer need PDF services (viewers
   do this for thunk-created engines automatically; module-scope singletons
   usually live for the app's lifetime and never need it).

If you are building a viewer interface, continue with the
[Headless documentation](https://www.embedpdf.com/docs/headless/react/getting-started) instead of
managing the Engine alone.
