This guide documents the architecture, configuration, operations, security model, and internals of NVM for Windows (nvm) — the Node.js version manager for Windows. It is intended for system administrators deploying NVM4W across machines, support engineers diagnosing failures, and developers who want to understand or contribute to the codebase. End-user task instructions live in the companion User Guide.
NVM for Windows is a single, self-contained Go executable. It has no runtime dependency on Node.js, no service, no database, and no background daemon of its own. Its entire job is to manage a set of on-disk Node.js installations and a single Windows symlink that selects the active one.
1. Executive Summary
| Attribute | Detail |
|---|---|
| Purpose | Install, switch, and manage multiple Node.js versions on Windows |
| Language | Go (module nvm, go 1.18) |
| Distribution | Single nvm.exe + companion author-nvm.exe, packaged with an Inno Setup installer |
| Runtime dependencies | None (no Node.js required); relies on Windows APIs |
| State model | Filesystem only — version folders + a single symlink + a plain-text settings.txt |
| Privilege requirement | Administrator or Windows Developer Mode (to manage the symlink) |
| Network endpoints | nodejs.org (Node + version index), github.com/npm/cli (npm), GitHub API (self-update), author.io (alerts) |
| Configuration | settings.txt in NVM_HOME + two environment variables (NVM_HOME, NVM_SYMLINK) |
| Supported consoles | Command Prompt, PowerShell (officially) |
Operating principle: every Node.js version lives in its own directory under NVM_HOME (e.g. C:\nvm\v22.14.0). The path NVM_SYMLINK is added to the system PATH exactly once at install time. Switching versions re-points that symlink — so the change is global, instant across all shells, and persists across reboots, without ever re-editing PATH.
2. Architecture
2.1 Component overview
┌─────────────────────────────────────────┐
User terminal ──────▶ │ nvm.exe (package main, src/nvm.go) │
(Admin shell) │ os.Args switch-dispatch CLI │
└───────────────┬─────────────────────────┘
│ calls internal packages
┌─────────────┬──────────────┬────┴───────┬─────────────┬───────────────┐
▼ ▼ ▼ ▼ ▼ ▼
nvm/web nvm/node nvm/arch nvm/file nvm/utility nvm/upgrade
downloads, version PE-header unzip, debug logs, self-update,
mirrors, discovery, arch detect settings I/O cross-vol move notifications,
proxy, SSL install state scheduled tasks
│ │ │
▼ ▼ ▼
nodejs.org %NVM_HOME%\vX.Y.Z\ nvm/author ──▶ author-nvm.exe
github npm (per-version node.exe + npm) (companion: notifications)
│
▼
NVM_SYMLINK ──(on system PATH)──▶ re-targeted on `nvm use`
2.2 Package inventory
| Package | File(s) | Responsibility |
|---|---|---|
main |
src/nvm.go |
CLI entry point, command dispatch, install/use/uninstall orchestration, settings persistence, debug troubleshooter, elevation helpers |
nvm/web |
src/web/web.go |
HTTP client, proxy + SSL config, mirror resolution, Node/npm downloads, redirects, connectivity ping, zip extraction with ZipSlip guard |
nvm/node |
src/node/node.go |
Detect current version/arch, enumerate installed versions, fetch & classify the remote version index (LTS/current/stable/unstable), map Node→npm versions |
nvm/arch |
src/arch/arch.go |
Architecture detection by scanning the PE header bytes of node.exe; validate/normalize 32/64/arm64 |
nvm/file |
src/file/file.go |
Unzip, ReadLines, Exists helpers |
nvm/semver |
src/semver/semver.go |
Vendored semantic-version parsing/validation/comparison |
nvm/upgrade |
src/upgrade/{check,notification,register,upgrade}.go |
Self-update pipeline, release/alert checks, toast notifications, Windows Task Scheduler registration |
nvm/utility |
src/utility/{logging,rename}.go |
Debug logging (ANSI), cross-volume Rename (move-or-copy) |
nvm/encoding |
src/encoding/encoding.go |
Charset detection and UTF-8 conversion for settings values |
nvm/author |
src/author/bridge.go |
Bridge to the external author-nvm.exe companion (notifications, subscribe, upgrade hooks) |
2.3 Command dispatch model
There is no CLI framework (no Cobra, no flag package for subcommands). main() reads os.Args and routes on args[1] with a switch. The implication for operators and contributors: the authoritative list of commands is the switch block in src/nvm.go:main() plus the hard-coded usage text in help(). Positional arguments are interpreted as: args[1] = command, args[2] = version/detail, args[3] = architecture (32/64/arm64) when applicable.
The init() function additionally registers a custom nvm:// URL-protocol handler. When nvm.exe is launched with a nvm://launch?action=... argument (from a toast-notification button), it rewrites os.Args into the equivalent command (install, use, upgrade) before main() runs.
Note
Any argument equal to
verbose(dashes stripped) anywhere on the command line enables debug logging viautility.EnableDebugLogs(). This is parsed ininit(), independent of the command.
2.4 The symlink switching mechanism
This is the core design decision (chosen over the .bat-shim and PATH-rewriting approaches used by older Windows version managers):
- At install time,
NVM_SYMLINK(e.g.C:\nvm\node) is added to the systemPATH. nvm use Xvalidates the target, removes the existing symlink (rmdirvia elevation), and creates a new directory symlink (mklink /D) pointing at%NVM_HOME%\vX.- Because the symlink — not the version folders — is on
PATH, the activenode/npmchange everywhere at once and survive reboots.
Architecture selection is handled by renaming executables inside the active version folder: node32.exe/node64.exe are swapped to/from node.exe so the on-PATH node matches the requested bitness.
3. System Requirements
| Requirement | Detail |
|---|---|
| OS | Windows (10/11 and Server editions). Uses kernel32.dll/user32.dll and golang.org/x/sys/windows |
| Privileges | Administrator, or Windows Developer Mode enabled (AllowDevelopmentWithoutDevLicense registry value > 0) |
| Console | Command Prompt or PowerShell (officially supported) |
| Network | Outbound HTTPS to nodejs.org, github.com, api.github.com, and author.io (or configured mirrors/proxy) |
| Disk | Each Node.js version is a full install (tens to hundreds of MB); plan capacity per version retained |
| Go toolchain (build only) | Go ≥ 1.18 to compile from source |
Important
NVM_HOMEandNVM_SYMLINKmust be different paths, andNVM_SYMLINKmust point at a location that does not already exist as a physical directory. Thedebugcommand enforces both invariants and warns if violated.
4. Configuration Reference
4.1 Environment variables
These are read directly from the process environment (os.Getenv). The installer sets the first two.
| Variable | Source | Purpose |
|---|---|---|
NVM_HOME |
src/nvm.go |
Root directory for installed versions and settings.txt. settings.txt is resolved as %NVM_HOME%\settings.txt. |
NVM_SYMLINK |
src/nvm.go |
The symlink path placed on system PATH; its target is swapped on nvm use. |
PROCESSOR_ARCHITECTURE |
src/nvm.go |
Seeds the default architecture when none is configured. |
PATH |
src/nvm.go |
Inspected by nvm debug to detect conflicting Node installs and confirm the symlink is present. |
APPDATA |
src/upgrade/notification.go |
Location of notification state: %APPDATA%\.nvm\.updates.json (hidden). |
4.2 settings.txt
The persistent configuration file, stored at %NVM_HOME%\settings.txt. It is a plain-text, colon-delimited key/value file (one pair per line) — not YAML or JSON. It is written by saveSettings() and parsed by setup(). Values are passed through os.ExpandEnv, so environment-variable references like %APPDATA% are expanded on read.
Example (examples/settings.txt):
root: C:\Users\Corey\AppData\Roaming\nvm
path: C:\Program Files\nodejs
arch: 64
proxy: none
| Key | Written by | Meaning |
|---|---|---|
root |
nvm root, install/use |
Storage root for version folders (mirrors env.root). |
path |
installer / setup() |
The original Node.js path captured at install (originalpath). |
arch |
nvm arch <32|64> |
Default architecture (32/64/arm64). |
proxy |
nvm proxy <url> |
Download proxy URL, or none. |
node_mirror |
nvm node_mirror <url> |
Override for the Node download base URL. |
npm_mirror |
nvm npm_mirror <url> |
Override for the npm download base URL. |
originalversion |
installer / setup() |
Version of the pre-existing Node.js captured at install. |
Caution
Do not hand-edit
settings.txtto pointroot/symlink at a real Node.js directory.validSymlink()deliberately refuses to delete a path that is a physical file/directory rather than a symlink — an anti-footgun added after users corrupted installs by manual edits. Use thenvmcommands instead.
4.3 Download endpoints and defaults
Defined in src/web/web.go:
| Setting | Default | Override |
|---|---|---|
| Node base address | https://nodejs.org/dist/ |
nvm node_mirror <url> |
| npm base address | https://github.com/npm/cli/archive/ |
nvm npm_mirror <url> |
| Version index | <node_base>/index.json |
follows the node mirror |
| Proxy | none | nvm proxy <url> |
| SSL verification | on | --insecure flag on install (per-invocation only) |
Mirror URLs are normalized: a missing scheme gets http:// prepended, and a trailing / is enforced.
5. Operations
5.1 Installation and provisioning
Deploy via the Inno Setup installer (nvm-setup.exe) built from nvm.iss. The installer:
- Writes
nvm.exeandauthor-nvm.exeto the install directory. - Sets
NVM_HOMEandNVM_SYMLINKenvironment variables. - Adds
NVM_SYMLINKto the systemPATH. - Optionally absorbs an existing Node.js installation (copying its files and global modules into the NVM root, since Windows forbids cross-vendor uninstalls).
For fleet deployment, standardize NVM_HOME/NVM_SYMLINK paths and pre-remove any baseline Node.js image to avoid PATH conflicts (the #1 support issue).
5.2 Health check — nvm debug
nvm debug (checkLocalEnvironment()) is the primary operational diagnostic. It reports and validates:
- Administrator vs. elevated vs. standard rights, and Windows Developer Mode status.
PATHconflicts — anothernode.exeahead of the symlink.- Console type (warns if not Command Prompt/PowerShell).
- Windows build number (via
RtlGetNtVersionNumbers). - NVM4W version, author-bridge version, resolved binary path, settings path.
NVM_HOME/NVM_SYMLINKvalues, and that they differ and are not nested.- Whether the symlink exists and actually is a symlink pointing at a directory.
- IPv6 status (flagged as a possible download-slowness cause).
- Reachability of the Node version index (accounting for mirror/proxy).
- Installed versions missing
node.exe(invalid) ornpm.cmd(Node-only). - Presence of
settings.txt. - Availability of an NVM4W self-update.
A clean machine prints No problems detected.; otherwise a PROBLEMS DETECTED block lists each issue with a remediation hint. Use this as the first step in any support ticket.
5.3 Logs
- Fatal errors are appended to
error.lognext tonvm.exe(writeToErrorLog), and some upgrade/scheduling paths writeerror.login the working directory. - Verbose/debug logging is opt-in: add
verboseto any command. Output is ANSI-colored (utility.DebugLogf) and includes the resolved command, architecture, download URLs, and file moves.
Tip
When collecting diagnostics for a bug report, re-run the failing command with
verboseappended and attach both the console output and anyerror.lognext tonvm.exe.
5.4 Self-update lifecycle
nvm upgrade (upgrade.Run → run) performs:
- Query the GitHub releases API (
api.github.com/repos/coreybutler/nvm-windows/releases/latest) and the alerts feed (author.io/nvm4w/feed/alerts). - Compare the running version against the latest; abort early if already current.
- Download
nvm-noinstall.zipplus its.checksum.txt; verify with an MD5 comparison (abort on mismatch). - Extract to a temp dir; download any extra assets (e.g.
update.exe). - Zip the current install as a backup and copy it to a hidden
.update\nvm4w-backup.zip. - Copy new files over the install directory; stage the new
nvm.exeunder.update\. - Write and launch
updater.bat, which waits for the current PID to exit, swaps in the newnvm.exe, schedules aRemoveNVM4WBackupcleanup task (~7 days out), and relaunches via thenvm://launch?action=upgrade_notifyprotocol.
Warning
Negative knowledge — rollback is not wired up in this build. The user-facing help text advertises "Manual rollback available for 7 days after upgrade," and the backup is created and retained for 7 days. However, the
rollbackargument handler insideupgrade.run()is entirely commented out in this commit. To revert, reinstall the prior release from the GitHub releases page; do not rely on annvm upgrade rollback-style command — it does not exist here.
5.5 Notifications and scheduled checks
nvm subscribe <topic> (topics: lts, current, nvm4w, author) registers Windows scheduled tasks via schtasks (src/upgrade/register.go), each running nvm checkForUpdates <topic> hourly at a staggered minute:
| Topic | Scheduled task name | Cadence |
|---|---|---|
lts |
NVM for Windows Node.js LTS Update Check | hourly @ :30 |
current |
NVM for Windows Node.js Current Update Check | hourly @ :25 |
nvm4w |
NVM for Windows Update Check | hourly @ :15 |
author |
NVM for Windows Author Update Check | hourly @ :45 |
Notification de-duplication state lives in %APPDATA%\.nvm\.updates.json (hidden). Toast notifications and the actual notification delivery route through the external author-nvm.exe via nvm/author.Bridge. nvm unsubscribe <topic> removes the corresponding scheduled task.
5.6 Backup and recovery
- Node versions are independent directories under
NVM_HOME; backing up that tree backs up all installed runtimes. - Configuration is a single
settings.txt. - NVM4W binary backups are produced automatically on upgrade (
.update\nvm4w-backup.zip, 7-day retention). - To recover a corrupted version:
nvm reinstall <version>. To recover NVM4W itself: re-run the installer for the desired release.
6. Security Analysis
| Area | Finding |
|---|---|
| Privilege model | Requires Administrator or Developer Mode to manage the symlink. Elevation is performed via elevatedRun → elevate.cmd/elevate.vbs helpers, with a fallback that retries through UAC. |
| TLS | HTTPS by default with certificate verification on. --insecure sets InsecureSkipVerify for a single install invocation and prints an explicit warning; it does not persist. |
| Update integrity | Self-update validates the download against a published checksum — but with MD5, which is cryptographically weak. It guards against corruption, not a determined tamper. There is no signature verification of the update payload itself (the distributed installer is Authenticode-signed for releases other than v1.1.8). |
| Archive extraction | web.unzip includes a ZipSlip guard (illegal file path on traversal outside the destination). The simpler file.Unzip helper only rejects names containing .. — a weaker check; prefer the web path for untrusted archives. |
| Symlink safety | validSymlink() refuses to delete a target that is a physical file/directory rather than a symlink, preventing accidental destruction of real Node.js installs from a bad settings.txt. |
| Network trust | Downloads come from nodejs.org and the npm GitHub archive over TLS; mirrors and proxies are user-configurable and trusted as configured. The alerts feed is fetched from author.io. |
| Privilege of scheduled tasks | subscribe creates schtasks entries that invoke nvm checkForUpdates; these run under the registering user's context. |
Caution
Treat
--insecureas a break-glass option for TLS-intercepting corporate proxies only. Because update integrity relies on MD5, do not depend on the checksum step as a tamper-evidence control on untrusted networks — verify you are pulling releases from the official GitHub project, and prefer the Authenticode-signed installer for distribution.
7. Code Quality & Patterns
- Concurrency:
install,use, andupgraderun their work in goroutines coordinated by aStatuschannel and async.WaitGroup, while a foreground goroutine renders progress (either console text or azenityprogress dialog when--show-progress-uiis set). Signal handling (SIGINT/SIGTERM) triggers rollback of partial installs. - Architecture detection is done by byte-scanning the PE header of
node.exe(arch.Bit:arm64=5045000064AA,x64=504500006486,x86=504500004C), rather than trusting filenames. - Cross-volume moves:
utility.Renamefalls back fromos.Renameto a recursive copy-then-delete when source and destination are on different drives — important because temp downloads and the NVM root may live on different volumes. Large-extract races are handled with exponential backoff (1/2/4/8/16s) when moving npm into place. - Vendored semver: the project vendors a
semverpackage (src/semver) in addition to importingblang/semver, used in the upgrade comparison paths. - Error surface: 92 distinct error strings were catalogued across the codebase (richest in
src/semverandsrc/nvm.go), giving precise, user-actionable messages — these double as the canonical troubleshooting catalog.
Conventions for contributors
- Add new commands to the
switchinsrc/nvm.go:main()and to thehelp()usage text — they are maintained by hand and must stay in sync. - Positional argument slots are fixed (
args[2]= version,args[3]= arch); preserve this contract. - Keep new user-facing failures as explicit
errorstrings with remediation guidance, consistent with the existing catalog.
8. Build & Contributing
The build is driven by src/manifest.json (name, version, build entrypoint nvm.go, output ../bin, minify, release/dev profiles) and an Inno Setup script (nvm.iss). A Deno helper, build.js, injects the manifest version into nvm.iss and invokes assets\buildtools\iscc.exe to compile the installer.
From source (per the README):
# Install Go from https://golang.org
git clone https://github.com/coreybutler/nvm-windows
cd nvm-windows\src
go get github.com/blang/semver
go get github.com/olekukonko/tablewriter
build.bat
# resulting setup program lands in the dist directory
Note
The build deliberately uses Go (and a Deno helper for the installer) rather than Node.js — the author's stated rationale is that a Node version manager should not depend on the tool it installs.
Key dependencies
| Dependency | Role |
|---|---|
github.com/blang/semver |
Semantic version parsing/comparison |
github.com/olekukonko/tablewriter |
Renders the nvm list available table |
github.com/ncruces/zenity |
Native progress dialogs for protocol-launched installs/upgrades |
github.com/go-toast/toast |
Windows toast notifications |
github.com/dustin/go-humanize |
Human-friendly relative release dates in notifications |
github.com/saintfish/chardet |
Charset detection for settings encoding |
github.com/coreybutler/go-fsutil, go-where |
Filesystem move helpers and executable lookup |
golang.org/x/sys/windows |
Windows registry, console mode, token/privilege APIs |
9. Potential Improvements
These are observations drawn from the source as it stands at this commit.
- Wire up rollback. The 7-day backup is created and advertised in help text, but the
rollbackhandler is commented out. Either implement the restore path or correct the help text to avoid setting a false expectation. - Strengthen update integrity. Replace the MD5 update checksum with SHA-256, and consider verifying a signature over the payload rather than relying on the installer's Authenticode signature alone.
- Unify zip extraction. Two unzip implementations coexist (
web.unzipwith a full ZipSlip guard,file.Unzipwith only a..substring check). Consolidate on the hardened version. - Formalize the command surface. The hand-maintained
switch/help()pairing is easy to desynchronize. A small command-registry table (name, aliases, handler, help) would remove the duplication and make--version/alias handling uniform. - Externalize
settings.txtparsing edge cases. The colon-delimited parser splits on the first:and rejoins the remainder, which is correct for Windows paths but is custom logic worth unit-testing against values containing:and expanded env vars. - arm64/64 availability heuristics (
IsNode64bitAvailable,IsNodeArm64bitAvailable) carryTODO: fixmeand optimistically assume availability above a version threshold. A definitive check against the version index would prevent misleading "only available in 32-bit" errors.
10. Quick Reference
Files and locations
| Path | Purpose |
|---|---|
%NVM_HOME%\settings.txt |
Persistent configuration |
%NVM_HOME%\vX.Y.Z\ |
A single installed Node.js version |
%NVM_SYMLINK% |
Active-version symlink (on system PATH) |
<install dir>\nvm.exe |
Main executable |
<install dir>\author-nvm.exe |
Notification/subscribe companion |
<install dir>\error.log |
Fatal error log |
<install dir>\.update\nvm4w-backup.zip |
Pre-upgrade backup (hidden, 7-day retention) |
%APPDATA%\.nvm\.updates.json |
Notification de-dup state (hidden) |
Network endpoints
| Endpoint | Used for |
|---|---|
https://nodejs.org/dist/ (+ index.json) |
Node downloads and the version index |
https://github.com/npm/cli/archive/ |
npm downloads |
https://api.github.com/repos/coreybutler/nvm-windows/releases/latest |
Self-update check |
https://author.io/nvm4w/feed/alerts |
Upgrade alerts/warnings |
Operational commands
| Command | Operator use |
|---|---|
nvm debug |
First-line diagnostic for any issue |
nvm root [path] |
Inspect/relocate the version store |
nvm upgrade |
Update NVM4W itself (no rollback command — reinstall to revert) |
nvm proxy / node_mirror / npm_mirror |
Configure for restricted networks |
nvm subscribe / unsubscribe <topic> |
Manage background update-check scheduled tasks |
nvm install <v> all |
Provision both 32- and 64-bit of a version |
Architecture availability rules
| Architecture | Available from |
|---|---|
| 32-bit | All supported versions |
| 64-bit | Node 8+ (older majors are 32-bit only) |
| arm64 | Node 19.9+ |