"Three Node features that did not exist three years ago. Each replaces a category of dependency. Together they shift what 'a Node project' even means."
The Permissions Model
Node has always run with full OS privileges by default. fs.readFile('/etc/passwd') works if your process is allowed to read that file. The permissions model (stabilized Node 24) inverts that: you opt INTO permissions when starting Node, and any code that exceeds them throws.
node --permission --allow-fs-read=./data --allow-net=api.example.com server.mjs
This server can read ./data, can fetch api.example.com, and nothing else. fs.readFile('/etc/passwd') throws ERR_ACCESS_DENIED. net.connect('evil.example') throws. Even a transitive dependency that tries to phone home gets blocked.
Available flags: --allow-fs-read, --allow-fs-write, --allow-net, --allow-worker, --allow-child-process, --allow-addons. Each takes a comma-separated list or wildcard. The mental model: capabilities, declared at startup, immutable afterward.
Single Executable Applications (SEA)
SEA turns your Node script into a standalone executable — no node command, no npm install for users. ./my-cli runs on a machine that doesn't have Node at all.
# 1. Write your script
# script.mjs
console.log('hi from a bundled Node app');
# 2. Bundle it into a blob
echo '{ "main": "script.mjs", "output": "sea-blob.blob" }' > sea-config.json
node --experimental-sea-config sea-config.json
# 3. Inject the blob into a copy of the node binary
cp $(command -v node) my-cli
npx postject my-cli NODE_SEA_BLOB sea-blob.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
# 4. Now ./my-cli is a standalone executable
./my-cli
SEA is what cwkPippa would use to ship the Cinder bridge as a single binary — no "please install Node first" friction for users. The output is ~100MB (the full Node binary), but it works.
node:sqlite — Database Without a Dependency
node:sqlite:import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync('./pippa.db');
db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY,
body TEXT NOT NULL,
created_at INTEGER
);
`);
const insert = db.prepare(
'INSERT INTO messages (body, created_at) VALUES (?, ?)'
);
insert.run('hi from Pippa', Date.now());
const rows = db.prepare('SELECT * FROM messages').all();
console.log(rows);This replaces better-sqlite3 (the npm staple) for many use cases. It's the same C library underneath, bundled by Node itself. Important: the API is synchronous — DatabaseSync exists because async SQLite drivers historically caused more confusion than they solved. SQLite operations are usually fast; sync is fine for most workloads. If you need async, libraries that wrap node:sqlite are appearing.Why These Matter Together
Each feature on its own is incremental. Together they shift what shipping a Node app looks like in 2026:
- Write your code in TypeScript, with type-strip-mode for execution.
- Use
node:sqlitefor storage, no dependency. - Run with
--permissionto constrain what your code can do. - Bundle as SEA so users run a single binary.
The whole flow from source to user: zero npm dependencies for runtime, zero install steps for users, security constraints baked in. This wasn't possible in Node 18; in Node 26 it's straightforward. Not every project benefits, but for CLIs, small services, and embedded use cases, the calculus has changed.