NodeJSSingle Executable Applications

Single Executable Applications

Node.js 20 (stable in 21.7) supports Single Executable Applications (SEA) — packaging your JavaScript application and the Node.js runtime into a single self-contained binary. The binary can be distributed and run on a target machine without Node.js installed. This is useful for CLI tools, desktop utilities, and any application where you want zero-dependency distribution. The approach differs from bundlers (esbuild, webpack) which bundle your JS but still require Node to run; SEA embeds Node itself.

The build process

Text
Build steps for a SEA binary:

1. Bundle your JS into a single file (esbuild/rollup)
   └─ src/cli.ts → dist/cli.js (one file, no node_modules dependency)

2. Create a SEA config JSON:
   └─ { "main": "dist/cli.js", "output": "sea-prep.blob" }

3. Generate the blob:
   └─ node --experimental-sea-config sea-config.json
   └─ sea-prep.blob (your app, serialized for injection)

4. Copy the node binary:
   └─ cp $(node -e "process.execPath") my-cli

5. Inject the blob into the binary:
   └─ npx postject my-cli NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2

6. (macOS) Sign the binary:
   └─ codesign --sign - my-cli
Step-by-step with a real CLI

Bash
# 1. Bundle your app into a single JS file with esbuild:
npx esbuild src/cli.ts --bundle --platform=node --outfile=dist/cli.js

# 2. Create sea-config.json:
cat > sea-config.json << 'EOF'
{
  "main": "dist/cli.js",
  "output": "sea-prep.blob",
  "disableExperimentalSEAWarning": true
}
EOF

# 3. Generate the blob:
node --experimental-sea-config sea-config.json

# 4. Copy the Node binary:
cp $(node -e "process.execPath") my-cli
# On Windows: cp (node -e "process.execPath") my-cli.exe

# 5. Remove existing signature (macOS only):
codesign --remove-signature my-cli

# 6. Inject the blob:
npx postject my-cli NODE_SEA_BLOB sea-prep.blob \
  --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
  --macho-segment-name NODE_SEA   # macOS only

# 7. Re-sign (macOS only):
codesign --sign - my-cli

# 8. Run:
./my-cli --help
(node:12345) ExperimentalWarning: Single executable application is an experimental feature...
Usage: my-cli [options]
sea-config.json options

JSON
{
  "main": "dist/cli.js",
  "output": "sea-prep.blob",
  "disableExperimentalSEAWarning": true,
  "useSnapshot": false,
  "useCodeCache": true,
  "assets": {
    "logo.png": "./assets/logo.png",
    "config.json": "./default-config.json"
  }
}

TS
// Accessing bundled assets from inside the SEA:
import { getAsset, getAssetAsBlob, getRawAsset } from 'node:sea'
import { isSea } from 'node:sea'

if (isSea()) {
  // Running as a SEA binary
  const configBuffer = getRawAsset('config.json')     // ArrayBuffer
  const config = JSON.parse(Buffer.from(configBuffer).toString('utf8'))
} else {
  // Running as a regular Node process (development)
  const config = JSON.parse(fs.readFileSync('./default-config.json', 'utf8'))
}
SEA vs third-party packaging tools

Tool

Approach

Pros

Cons

Node SEA (built-in)

Inject JS blob into a copy of the Node binary

No extra tooling, official support

Experimental; binary is ~90MB (includes full Node)

pkg (Vercel)

Embed Node + app into custom executable

Mature, wide adoption

Unmaintained (archived by Vercel 2023)

nexe

Similar to pkg; patches Node binary

Active community

Complex build; compiler required

Bun bun build --compile

Bundle + embed Bun runtime

Smallest binary, fast

Different runtime (Bun, not Node); compatibility considerations

SEA binaries include the full Node.js runtime — the output binary is typically 80–100 MB; consider this before choosing SEA for distribution over a simple npm package
A Node SEA binary copies the entire Node.js executable (typically 80–100 MB depending on platform) and injects your application code into it. For a CLI tool distributed via npm, this is far larger than necessary — an npm package lets users leverage the Node they already have. SEA makes sense when: (1) the target machine won't have Node installed, (2) you need to prevent modification of your code, or (3) you're distributing to non-technical users who can't install Node. For developer tools distributed via npm or npx, stick with regular packages.
Cross-platform build matrix

Bash
# Build for multiple platforms in CI (GitHub Actions example):
# strategy:
#   matrix:
#     os: [ubuntu-latest, macos-latest, windows-latest]
#
# Each job:
# 1. checkout + npm ci
# 2. esbuild bundle
# 3. node --experimental-sea-config sea-config.json
# 4. Platform-specific inject + sign steps
# 5. Upload artifact

# The resulting binaries:
# my-cli-linux-x64    (no extension)
# my-cli-macos-x64    (codesigned)
# my-cli-win-x64.exe
Next
A concise reference of Node.js essentials: [Node.js Cheat Sheet](/nodejs/cheatsheet).