Skip to content

Architecture

How pkg turns a Node.js project into a single executable, at a glance.

Looking for deeper internals?

This page is the short version — build pipelines, binary layout, VFS provider, worker-thread bootstrap, and patch tables all live in docs/ARCHITECTURE.md. Read that one if you're contributing to pkg or debugging the runtime.

The two modes

pkg supports two packaging strategies, selected via the --sea flag:

bash
pkg .                     # Traditional mode (default)
pkg . --sea               # Enhanced SEA mode (Node ≥ 22 with package.json)
pkg single-file.js --sea  # Simple SEA mode (single .js file)

Both start from the same project and end with one executable, but they take very different paths:

AspectTraditionalEnhanced SEA
Node.js APIBinary patchingOfficial node:sea
Base binaryPatched (pkg-fetch)Stock
VFSCustom binary format@roberts_lando/vfs
BytecodeV8 compiledSource as-is
ESMTransformed to CJSNative
CompressionBrotli / GZipNone
Code strippingYes (sourceless)No (plaintext)

See SEA vs Standard for the full decision guide.

Traditional mode in one paragraph

The walker parses the entry file, resolves every require/import, transforms ESM to CJS, and compiles each module to V8 bytecode. Files are serialized into "stripes" (path + store type + data), optionally compressed, and injected into a patched Node.js binary at placeholder offsets. At runtime, the injected bootstrap.js patches fs, Module, child_process, and process.dlopen so that anything inside /snapshot/ is served from the payload instead of disk.

Strength: V8 bytecode can be stored without source, so code is not trivially extractable. Weakness: the patched binary comes from pkg-fetch, which lags behind upstream Node.js releases.

Enhanced SEA mode in one paragraph

The walker runs with seaMode: true — no bytecode, no ESM transform. All files are concatenated into a single __pkg_archive__ blob with a __pkg_manifest__.json that maps each path to [offset, length]. node --experimental-sea-config produces a prep blob, which postject injects into a stock Node.js binary as a NODE_SEA_BLOB resource. At runtime, a small bootstrap loads the archive via sea.getRawAsset() (zero-copy) and mounts it through @roberts_lando/vfs (or node:vfs once it lands upstream).

Strength: uses official Node.js APIs, stock binaries, future-proof. Weakness: source code is stored in plaintext.

The virtual filesystem

Both modes expose packaged files under a /snapshot/ prefix. User code calling fs.readFileSync('/snapshot/app/config.json') is intercepted and served from the payload:

Traditional mode patches ~20 fs functions by hand inside bootstrap.js. SEA mode delegates to @roberts_lando/vfs, which intercepts 164+ functions and hooks the module resolution system automatically.

Native addons (.node files) can't be loaded straight from memory, so both modes extract them to ~/.cache/pkg/<sha256>/ on first load and call the real process.dlopen against the extracted path. See Native addons.

Worker threads

Workers spawned from a packaged app don't inherit the main thread's fs patches. SEA mode solves this by monkey-patching the Worker constructor: if the worker script is inside /snapshot/, pkg reads the source through the VFS, prepends the VFS bootstrap, and spawns the worker with { eval: true } so it runs in memory with the same VFS mounted.

Which files matter

If you want to read the source, these are the entry points:

FileRole
lib/index.tsCLI entry, mode routing
lib/walker.tsDependency walker (traditional + SEA)
lib/packer.ts + producer.tsTraditional payload assembly + binary injection
lib/sea.ts + sea-assets.tsSEA orchestrator + archive/manifest generation
prelude/bootstrap.jsTraditional runtime (fs/Module/process patching)
prelude/sea-bootstrap.jsSEA runtime (CJS wrapper for CJS + ESM/TLA entries)
prelude/sea-vfs-setup.jsSEA VFS core: SEAProvider, archive loading, Windows patches
prelude/bootstrap-shared.jsShared patches: dlopen, child_process, process.pkg

For line-by-line pipelines, binary layouts, patch tables, and the upstream Node.js dependency map, see docs/ARCHITECTURE.md.

Where pkg is heading

The long-term goal is to eliminate patched Node.js binaries entirely and ship pkg on stock Node via SEA + node:vfs. Progress is tracked in #231.

Once node:vfs lands upstream, @roberts_lando/vfs becomes a fallback and the SEA bootstrap shrinks to almost nothing — the VFS provider and manual mount are no longer needed.

Released under the MIT License.