NW.js Context Modes: The One Decision That Shapes Your Whole App
NW.js lets DOM JavaScript call Node directly — but whether those worlds share one context is the decision that determines your app's security posture. Here's how to structure around it.
09 Mar 2026, 13:11 UTC

If you build a desktop app with NW.js, the first real engineering decision you make is not the UI framework or the build tooling. It is the context mode — whether your Node.js code and your DOM code share one JavaScript world or live in separate ones. Get this wrong and you either fight confusing bugs in mixed context, or you leave a page that loads remote content with a direct line to fs and child_process. The good news: the decision is easy to make once you understand what each mode actually gives you.
What "dual context" actually means
NW.js embeds Node.js directly alongside the Chromium renderer. A page loaded from your app manifest can call require('fs') from ordinary DOM JavaScript — no backend process, no IPC layer you have to write yourself. That is the framework's main appeal for small tools.
But there are two ways to wire those worlds together:
- Mixed context: Node and the DOM share a single JavaScript context. Any script on the page can
require()anything. Simple, but anything that runs in that page — including injected or remote content — inherits full Node power. - Separate context: Node code runs in its own context, isolated from the page. The page cannot call
require()directly; you expose a narrow, explicit bridge instead.
Mixed context is seductive because the "hello world" is three lines long. It is also how people ship apps where a pasted HTML snippet or a compromised CDN script can read the user's home directory.
A worked example: a Markdown notes app
Say you are building a small notes app. The renderer shows a textarea and a list of notes; Node's fs module reads and writes .md files in a user data directory. The entry point is the manifest:
{
"name": "md-notes",
"main": "index.html",
"window": {
"width": 900,
"height": 600,
"frame": true
}
}In mixed context, index.html can just do this in a script tag:
const fs = require('fs');
fs.writeFileSync('/path/to/note.md', text);It works, and for a toy it is fine. The problem is that everything on that page now has fs. The safer structure in separate context mode is a small bridge that exposes only two operations:
// node-side bridge, loaded in the Node context
const fs = require('fs');
const path = require('path');
const notesDir = path.join(nw.App.dataPath, 'notes');
global.bridge = {
saveNote(name, text) {
const safe = path.basename(name); // block path traversal
fs.writeFileSync(path.join(notesDir, safe), text, 'utf8');
},
loadNote(name) {
const safe = path.basename(name);
return fs.readFileSync(path.join(notesDir, safe), 'utf8');
}
};The page then calls bridge.saveNote('ideas.md', text) and never touches fs itself. Two things to notice: the bridge whitelists operations instead of exposing a module, and it sanitizes the filename so a crafted name like ../../.ssh/authorized_keys cannot escape the notes directory. That is the pattern to copy regardless of app size: narrow surface, validated inputs.
How to verify which mode you are actually in
Do not trust the docs from memory — verify on the runtime version you pinned:
- Create the minimal manifest above with
"main": "index.html"and callrequire('fs')from a script tag. In mixed context, file access works immediately. - Switch the manifest to separate context mode and reload. The direct
requirefrom the page should now fail, which is your confirmation that isolation is active and only the bridge path works. - If a window ever loads remote content, check that
node-remoteis not enabled for it. Remote content plus Node integration is the classic NW.js footgun.
One practical note: only SDK builds of NW.js ship DevTools. If you download the normal flavor and wonder why win.showDevTools() does nothing, that is why. Pin a specific runtime version in your project and test against it, because API behavior has shifted across major releases.
The trade-off you accept up front
Distribution means bundling your source with the NW.js runtime per platform. Tools like nwbuild automate producing Windows, macOS, and Linux binaries, but every output carries a full Chromium, so tens of megabytes is the floor, not the exception. For an internal tool that is irrelevant; for a consumer download it may not be.
Compared with Electron, NW.js's DOM-first Node integration is genuinely convenient for small utilities — there is no preload/IPC boilerplate to write before your first file read. The cost is a smaller community, thinner documentation freshness, and more security hardening you do by hand. Electron's process model forces separation on you; NW.js lets you choose, which means you can also choose badly.
The closing rule of thumb
Use mixed context only when every byte of content in every window is local and trusted, and the app is small enough that a shared global namespace will not bite you. For anything else — anything that renders Markdown from disk, fetches a URL, or loads a third-party script — start in separate context, write a bridge with two or three whitelisted functions, and validate everything crossing it. Then verify the isolation by watching a bare require fail in the page. That five-minute check is the difference between "I think it is isolated" and knowing it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.