How Node.js Single Executable Applications Work

This article explains how the Single Executable Applications (SEA) feature in Node.js allows developers to bundle a JavaScript application and the Node.js runtime into a single, redistributable binary. It covers the underlying mechanism of the node:sea built-in module, the step-by-step process of generating a standalone executable, and how the runtime executes the embedded code without requiring an external Node.js installation on the target machine.

Understanding Single Executable Applications (SEA)

Historically, running a Node.js application required the target machine to have the Node.js runtime installed. The Single Executable Applications (SEA) feature, introduced natively in Node.js, eliminates this dependency. It allows developers to package their JavaScript code directly into a copy of the Node.js binary itself. When a user runs the resulting executable, the embedded Node.js runtime launches and immediately executes the bundled JavaScript code.

The Architecture of SEA

The SEA mechanism relies on injecting a preparation blob containing the application code into a dedicated section of the Node.js executable.

Instead of compiling JavaScript into machine code, SEA leverages the fact that the Node.js binary is designed to look for an embedded asset block inside its own executable file. If this resource block is present, Node.js bypasses its default behavior (which is to look for a command-line script argument or an index.js file) and instead executes the code stored within the embedded block.

Step-by-Step Execution Workflow

Creating a single executable application involves a distinct four-step pipeline.

1. Bundling the Source Code

Before creating an executable, all application code and its dependencies must be bundled into a single JavaScript file. Since the executable only reads a single entry point, tools like esbuild, Webpack, or Rollup are typically used to consolidate the project into a single dist/index.js file.

2. Creating the Configuration File

Node.js requires a JSON configuration file to define how the executable should be built. This file specifies the input path of the bundled JavaScript and the output path for the preparation blob. A typical sea-config.json looks like this:

{
  "main": "dist/index.js",
  "output": "sea-prep.blob"
}

3. Generating the Preparation Blob

Using the configuration file, developers run a Node.js command to compile the JavaScript code into a binary preparation blob:

node --experimental-sea-config sea-config.json

This command processes the entry point, checks for syntax, and packages the code into the sea-prep.blob file.

4. Injecting the Blob into the Binary

To create the final executable, a copy of the official Node.js executable is made. The preparation blob is then injected into this copy.

Depending on the operating system, the injection process varies: * Windows: The blob is resource-aligned and written into the binary using tools like signtool or PowerShell scripts. * macOS / Linux: A utility called postject is used to inject the blob into the binary’s asset segment (such as __bundle on macOS or .note.NODE_JS_LAND on Linux).

Once injected, the executable is ready to run. On macOS and Windows, the final binary must be re-signed to satisfy OS-level security policies.

How the Binary Runs

When the final executable is launched, the embedded Node.js runtime initializes. During initialization, Node.js checks its binary headers for the injected SEA segment.

If the segment is detected: 1. Node.js extracts the bundled JavaScript code into memory. 2. It configures the V8 engine and registers internal APIs. 3. It executes the JavaScript code within the embedded environment. 4. Command-line arguments passed to the executable are forwarded directly to the running JavaScript application via process.argv.

Through this process, Node.js delivers a self-contained, dependency-free executable suitable for distribution.