Ship a Node.js app as one executable file. Build it with node --build-sea in Node 26, or with the blob and postject flow in Node 24, and embed assets.

Bishal Biswas
Self Learner
What is a single executable application?
Normally, to run your Node program, a user needs Node installed, your code, and probably a node_modules folder. For a developer that is fine. For a colleague in accounts who just wants your report tool, it is a lot to ask.
A single executable application (SEA for short) is your app and the Node runtime packed into one file. The user downloads report-tool.exe (or report-tool on macOS and Linux), double-clicks or runs it, and it works. No Node install, no npm install.
Think of a ready meal. Instead of handing someone a recipe, a shopping list and a pan, you give them one box that already contains everything. They just heat it up.
Note: SEA is still marked as under active development in the Node documentation, so details change between versions. This lesson shows the flow for Node 24 and for Node 26.
How it works
A SEA is a copy of the node program with your script injected (written) into a special spot inside it. When that copy starts, it notices the embedded script and runs it instead of waiting for a filename.
Because the whole Node runtime is inside, the file is big: roughly the size of the node binary itself, so tens of megabytes even for “Hello world”.
The executable only runs on the platform it was built for. A Windows .exe will not run on macOS. Build once per platform you want to support.
Step 1: prepare one script
A SEA embeds one JavaScript file. Two rules for that file:
- Use CommonJS (
require). In Node 24 this is the only supported format. Node 26 adds an option for ES modules, covered below. - Only built-in modules can be required. Inside a SEA,
require()can loadnode:fs,node:pathand other built-ins, but not files from disk ornode_modules.
So if your app has several files or npm packages, first bundle it: use a bundler such as esbuild or Rollup to combine everything into one .js file. For this lesson, a single file is enough:
// hello.js
const { isSea } = require('node:sea');
console.log(`Hello, ${process.argv[2] ?? 'world'}!`);
console.log('Running as a single executable:', isSea());
Test it the normal way first:
node hello.js Asha
Hello, Asha!
Running as a single executable: false
The built-in node:sea module tells your code whether it is running inside an executable. isSea() returns false here because we ran it with plain node.
Building with Node 26: node –build-sea
Since Node 25.5, and so in every Node 26 release, Node can build the executable by itself with one command.
Create sea-config.json. Here output is the name of the finished executable:
{
"main": "hello.js",
"output": "hello"
}
On Windows, use "output": "hello.exe". The .exe extension is required there.
Then build:
node --build-sea sea-config.json
Node copies its own binary, injects hello.js into it, and writes hello (or hello.exe). Run it:
./hello world # macOS and Linux
.\hello.exe world # Windows
It prints the greeting just like before, but this time isSea() returns true, and no node command or hello.js file was needed.
Signing on macOS and Windows
macOS refuses to run binaries whose code signature does not match their contents, and injecting changes the contents. Re-sign it with an ad-hoc signature (a local signature that needs no certificate):
codesign --sign - hello
On Windows, signing is optional. An unsigned .exe runs, though Windows SmartScreen may warn users. If you have a code-signing certificate:
signtool sign /fd SHA256 hello.exe
Linux needs no signing step.
Building with Node 24: the blob and postject flow
Node 24 does not have --build-sea. If you try it, Node stops with bad option: --build-sea.
Instead, you build the executable in several steps. Node produces a preparation blob (a file holding your script in the format Node expects) and a separate tool, postject, injects it into a copy of node.
1. Make the blob
Here output is the blob file, not the executable:
{
"main": "hello.js",
"output": "sea-prep.blob"
}
node --experimental-sea-config sea-config.json
Wrote single executable preparation blob to sea-prep.blob
2. Copy the node binary
cp $(command -v node) hello # macOS and Linux
node -e "require('fs').copyFileSync(process.execPath, 'hello.exe')" # Windows
3. Remove the existing signature (macOS, optional on Windows)
codesign --remove-signature hello # macOS
signtool remove /s hello.exe # Windows, optional
4. Inject the blob with postject
postject is an npm package, so npx downloads and runs it. The long --sentinel-fuse value is a fixed marker string that Node looks for, copy it exactly:
# Linux
npx postject hello NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
# macOS: one extra option
npx postject hello NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
--macho-segment-name NODE_SEA
On Windows, in PowerShell, put it on one line:
npx postject hello.exe NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
5. Sign again and run
Sign on macOS (codesign --sign - hello), optionally on Windows (signtool sign /fd SHA256 hello.exe), then run ./hello world or .\hello.exe world.
That is the same result as --build-sea, with more manual steps. When your project moves to Node 26, you can replace steps 1 to 4 with the single command.
More config options
Both flows read the same kind of config file. Useful fields:
{
"main": "hello.js",
"output": "hello",
"disableExperimentalSEAWarning": true,
"useCodeCache": true,
"assets": {
"config.json": "./config.json"
}
}
disableExperimentalSEAWarning: hide the “experimental feature” warning that the executable otherwise prints when it starts.useCodeCache: embed V8’s compiled code cache so the app starts a little faster.assets: extra files to pack inside the executable. The key is the name you use in code, the value is the file on disk.
Node 26 adds "mainFormat": "module" to embed an ES module instead of CommonJS, and an executable field to choose which Node binary to build from (by default, the one running the command).
Reading embedded assets
Packed assets are read with node:sea, not node:fs, because they live inside the executable rather than on disk:
const sea = require('node:sea');
if (sea.isSea()) {
const config = JSON.parse(sea.getAsset('config.json', 'utf8'));
console.log(config.greeting);
}
getAsset(key, 'utf8')returns the file as a string. Without an encoding it returns anArrayBuffer.getAssetAsBlob(key)returns aBlob.
Calling getAsset outside an executable throws ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION, so check isSea() first, as the playground below does.
Common mistakes
Requiring your own files
const helpers = require('./helpers.js'); // fails inside a SEA
The embedded script cannot load files from disk. Bundle everything into one file first, or embed data files as assets.
Expecting one build to run everywhere
A SEA built on Windows produces a Windows executable. Build on each platform (a CI matrix with Windows, macOS and Linux runners is the usual way), or check the Node docs for cross-platform options.
Forgetting to sign on macOS
macOS, especially on Apple Silicon, refuses to run a modified binary whose signature no longer matches. Always run codesign --sign - after injecting.
Mixing up what output means
With --experimental-sea-config (Node 24), output is the blob. With --build-sea (Node 26), output is the finished executable. Keep a separate config file per flow if you need both.
Quick recap
- A SEA packs your script and the Node runtime into one executable file.
- The script must be one file. Use CommonJS in Node 24, and only
requirebuilt-ins. Bundle bigger apps first. - Node 26:
node --build-sea sea-config.jsonbuilds the executable directly.outputis the executable’s name. - Node 24:
node --experimental-sea-configmakes a blob, then copynode, inject withnpx postject, and sign. - Re-sign with
codesign --sign -on macOS. Signing is optional on Windows. - Embed extra files with
assetsand read them withrequire('node:sea').getAsset().
// greet.js: a CommonJS script ready to become a single executable app.
// Try it normally first: node greet.js Asha
// Then build it (Node 26): node --build-sea sea-config.json
// sea-config.json: { "main": "greet.js", "output": "greet", "assets": { "config.json": "config.json" } }
// (on Windows, write "greet.exe" as the output)
const sea = require('node:sea'); // built-ins are the only things require() can load inside a SEA
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
// Load settings: from the embedded asset inside the executable, or from disk otherwise
function loadConfig() {
if (sea.isSea()) {
return JSON.parse(sea.getAsset('config.json', 'utf8'));
}
const file = path.join(__dirname, 'config.json');
if (fs.existsSync(file)) {
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
return { greeting: 'Hello' }; // sensible default so the script always runs
}
const config = loadConfig();
const name = process.argv[2] ?? os.userInfo().username;
// When running as an executable there is no separate script file:
// process.execPath is the executable itself.
console.log(`${config.greeting}, ${name}!`);
console.log('Running as a single executable:', sea.isSea());
console.log('Program file:', path.basename(sea.isSea() ? process.execPath : __filename));
console.log('Node version baked in:', process.version);
console.log('Platform:', `${process.platform}-${process.arch}`);
// A tiny bit of real work: count words in a file passed as the second argument
const target = process.argv[3];
if (target) {
const text = fs.readFileSync(target, 'utf8');
const words = text.split(/\s+/).filter(Boolean).length;
console.log(`${path.basename(target)} has ${words} words`);
} else {
console.log('Tip: pass a file as the second argument to count its words');
}
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready