Skip to Content

From Node.js + npm

This is the easiest migration: 3va runs Node-style code (CommonJS, ESM, require("node:*")) and reads the node_modules npm already installed.

1. Run it on your existing node_modules

Keep installing with npm for now, and run the entry point with 3va:

npm install # as today 3va run server.js --allow-read=./node_modules --allow-net=127.0.0.1

Tested with an Express app on npm 11:

up 3456 $ curl 127.0.0.1:3456 {"lodash":"4.17.20","express":"4.22.3"}

The flags are the whole migration at this step:

  • --allow-read=./node_modules: loading a package is a file read. Without it: Permission denied: --allow-read=…/node_modules/express/index.js is required.
  • --allow-net=<host>: for the server and any outbound calls. See HTTP APIs.
  • --allow-env=NAME,…: every process.env.X you read. Without a grant the variable reads as undefined, so PORT or DATABASE_URL fall back to their defaults, but the read is reported (see below).

When a run is missing a grant, 3va lists what was refused on exit, with the exact flag to add:

[!] 2 permissions were denied during this run: connect to example.com:443 grant with: --allow-net=example.com:443 bind a local server on 0.0.0.0 needs any --allow-net grant (e.g. --allow-net=127.0.0.1) read environment variable DATABASE_URL grant with: --allow-env=DATABASE_URL

Add --trace-denials to see each refusal as it happens. To get the full set in one go, run the app once under observation and let 3va write it into package.json:

3va permissions learn server.js --write

It merges into the root "." scope and leaves per-package scopes alone. It also records a couple of variables the runtime itself reads (CLUSTER_WORKER, NODE_ENV), which you can delete. The result looks like this, and every command (3va run, 3va start, 3va test) picks it up:

package.json
{ "3va": { "no-prompt": true, "permissions": { ".": { "allow-read": ["./node_modules"], "allow-net": ["127.0.0.1"], "allow-env": ["PORT", "DATABASE_URL"] } } } }

2. Scripts

3va <name> runs package.json scripts when <name> isn’t a 3va command, through npm itself, outside the sandbox:

3va lint # → npm run lint

Because it runs outside the sandbox, it asks [y/N] first. In CI (no TTY) it is denied unless you pass --yes or set "3va": { "no-prompt": true }.

To run a script inside the sandbox, call the file directly: "start": "3va run server.js".

3. Switching installs to 3va

Convert the lockfile first, so you keep exactly the versions you have:

3va migrate --dry-run # Would migrate package-lock.json → 3va-lock.json (68 package(s)) 3va migrate 3va install --allow-net=registry.npmjs.org

In our test, express was locked at 4.18.2 under a ^4.18.0 range, and 3va install kept 4.18.2. Without a lockfile, ranges resolve to the highest matching version, as with npm.

In CI, use 3va ci. It installs exactly what 3va-lock.json records, checks integrity hashes, and fails if package.json has drifted:

Error: 3va-lock.json does not match package.json: lodash: package.json asks for 4.17.21, lockfile pins 4.17.20 Run `3va install` and commit the updated lockfile.

3va install scans every package for malware and doesn’t run lifecycle scripts unless you allow them. What to check before you switch:

  • The malware scanner can still block legitimate packages. Vite, rolldown and @types/node now pass, but oxlint, commander and several Expo packages don’t. After reviewing one, trust that exact version with "3va": { "trusted": ["oxlint@1.85.0"] }. It’s scanned again when the version changes.
  • Lifecycle scripts don’t run unless the package is in "3va": { "onlyBuiltDependencies": [...] }, and even then only when the script is a single JS file (it runs sandboxed). Shell commands like better-sqlite3’s prebuild-install || node-gyp rebuild are refused, so better-sqlite3, sharp, bcrypt and Puppeteer’s Chrome still end up without their binary. Packages that ship prebuilt binaries as platform-specific optional dependencies (esbuild, SWC, rollup, lightningcss) are installed.
  • node_modules/.bin is created, but many CLIs in it (vite, tsc, next) don’t run under 3va yet. Keep calling them through Node.

Details and workarounds are in the Migration Guide.

Things to keep in mind

  • 3va run has no --watch. Use 3va watch server.js.
  • The process manager replaces pm2: 3va start server.js --name api. Its permissions come from package.json, not flags (Process Manager).
  • engines.node is ignored. 3va doesn’t emulate a specific Node version. Test the APIs you rely on.