Migration Guide
This page pulls together what the per-tool and per-framework pages found: the order to migrate in, and every factor that decides whether a given part of your project can move now.
1. Take inventory
Answer these before touching anything:
| Question | Why it matters |
|---|---|
| What runs where? (server, scripts, tests, build, frontend) | Backends, scripts and tests move easily; build tooling doesn’t yet |
Any file:, git:, github: or tarball-URL dependencies? | 3va install rejects them (Invalid version 'file:./x'). Publish them to a registry or move them into a workspace |
| Which package manager and lockfile? | npm, Yarn v1, pnpm and bun.lock convert; Yarn Berry and bun.lockb don’t |
Any native dependencies built by install scripts? (sharp, bcrypt, better-sqlite3, Prisma) | Only allowlisted JS scripts run, so shell-script builds end up without their binary. 3va doctor --compat lists them |
| Which databases and auth methods? | Postgres SCRAM, pg.Pool and mysql2 don’t work yet |
Top-level await? deno.json? Bun.*? npm: specifiers? | None of them are supported yet |
| Which env vars, files, hosts and child processes does the app use? | Each one becomes an explicit grant |
2. Migrate in layers
- Run on your current
node_modules:3va run app.js --allow-read=./node_modules … - Test:
3va test - Deploy:
3va start(instead of pm2) in the environment where you deploy - Install:
3va migrate+3va install, and3va ciin CI, after checking Installing with 3va - Build tooling: stays on Node until the frameworks you use are supported
Each layer can ship on its own. If one causes trouble, go back one layer without undoing the others.
3. Permissions
This is the part of the migration that doesn’t exist with Node, Bun or npm-installed code.
-
Discover and record:
3va permissions learn app.js --writeruns the app with everything allowed and merges what it used intopackage.json, under the root"."scope. Existing per-package scopes are left alone. Exercise the code paths you care about while it runs, and review the diff: it also records a couple of variables the runtime itself reads (CLUSTER_WORKER,NODE_ENV).3va permissions suggestonly reads the source and is much broader. -
Declare the result in
package.jsonunder"3va"sorun,startandtestshare it and it’s reviewed like code:{ "3va": { "no-prompt": true, "permissions": { ".": { "allow-read": ["./node_modules", "./public"], "allow-write": ["./uploads"], "allow-net": ["127.0.0.1", "api.stripe.com:443"], "allow-env": ["PORT", "DATABASE_URL", "STRIPE_KEY"] } } } } -
Read the denial summary. When something was refused,
3va runlists it on exit with the flag to add, and--trace-denialsprints each refusal as it happens:[!] 4 permissions were denied during this run: read ./secret.txt grant with: --allow-read=./secret.txt write ./output.txt grant with: --allow-write=./output.txt spawn a child process grant with: --allow-child-process connect to localhost grant with: --allow-net=localhost -
Environment variables show up too. An ungranted
process.env.Xstill reads asundefined, so defaults kick in, but the run ends withread environment variable DATABASE_URL — grant with: --allow-env=DATABASE_URL, and in a terminal 3va asks at the moment of the read. Enumerating the environment (Object.keys(process.env)) just returns{}and isn’t reported. -
Network grants:
host:portlimits a grant to one port (--allow-net=api.stripe.com:443; a typo’d port matches nothing), forfetch, raw TCP and WebSocket alike.localhostand127.0.0.1are separate grants. -
listen(port)stays on127.0.0.1unless--allow-netincludes0.0.0.0,::or*. -
"no-prompt": truemakes local runs behave like CI (deny instead of asking), so you find missing grants before deploying.
4. Installing with 3va
3va install differs from npm on purpose: no install scripts, and a
malware scan. The workflow and each point to check:
3va migrate --dry-run # what would be converted
3va migrate # package-lock / yarn.lock v1 / pnpm-lock / bun.lock → 3va-lock.json
3va install --allow-net=registry.npmjs.org
3va ci --allow-net=registry.npmjs.org # in CI: exact lockfile, integrity-checked-
Lockfiles.
3va migrateconvertspackage-lock.json,yarn.lockv1,pnpm-lock.yaml(v9) and the textbun.lockwith versions, URLs and integrity hashes.3va installthen keeps the pinned versions. Yarn Berry lockfiles and the binarybun.lockbcan’t be converted, and 3va says why. -
Version resolution. Without a lockfile, ranges resolve to the highest matching version, as with npm.
3va install <pkg>saves^x.y.z(respectingsave-prefix/save-exact). -
CI.
3va cifails whenpackage.jsonand3va-lock.jsondisagree, and names the package, instead of silently re-resolving. -
Malware scanner. It rejects packages whose code mentions risky things (
child_process,.env,eval,curl | sh). Most mainstream tooling passes now (Vite, rolldown, TypeScript 6,@types/node, Expo, the Tauri CLI), butoxlint,commanderand several Expo/Metro packages are still rejected. Review a rejected package, then trust that exact version:{ "3va": { "trusted": ["oxlint@1.85.0"], "trustedIntegrity": { "oxlint@1.85.0": "sha512-…" } } }The pin expires by itself: when the version changes, the package is scanned again.
trustedIntegrityalso pins the bytes. A pin without a version never expires, and the install warns about it.--no-scanstill exists, but it turns the scan off for every package. -
The scanner is version-sensitive. A routine upgrade can suddenly fail to install (we saw
typescript@5.9.3rejected while 6.0.3 passed). -
Install scripts don’t run unless you allow them, per package:
{ "3va": { "onlyBuiltDependencies": ["esbuild", "better-sqlite3"] } }(pnpm’s
onlyBuiltDependenciesand Bun’strustedDependenciesare read too.) Even then, a script that is a single JavaScript file runs inside the sandbox, and a shell command is refused:⚠ better-sqlite3@12.2.0 install: "prebuild-install || node-gyp rebuild --release" is a shell command, not a single JavaScript entry point. 3va will not hand a package-authored string to a shell, even for an allowlisted package.So
better-sqlite3,sharp,bcryptand Prisma still end up without their binary. esbuild’spostinstallis a JS file, so it runs:⚙ postinstall script ran sandboxed for esbuild@0.25.12.3VA_ALLOW_SCRIPTS=1no longer enables scripts.To see which installed packages ship install scripts, run
3va doctor --compat:1 installed package(s) ship install-time scripts. 3va never runs them unless the project lists the package in "3va"."onlyBuiltDependencies": core-js@2.6.12 postinstall (not run) -
Prebuilt native binaries are installed. Platform-specific optional dependencies (
@esbuild/*,@rollup/rollup-*,@next/swc-*,lightningcss-*,@tauri-apps/cli-*) are picked byos,cpuandlibc, so a glibc Linux gets only the-gnubuild. -
node_modules/.binis created, but several CLIs in it don’t run under 3va yet:tsc(SyntaxError: Unexpected token ':'),vite,next. Keep them on Node. -
Registry dependencies only.
file:,git+https:,github:and tarball-URL specifiers are rejected withInvalid version 'file:./fake'. Use name@version format. Publish such packages to a (private) registry, or move them into a workspace and useworkspace:*. -
Mixed registries. The registry comes from
--allow-net. Install JSR (jsr.io) and npm (registry.npmjs.org) packages in separate commands. A combined command exits with code 1 for the npm ones, and only the packages that did install are added topackage.json. -
What carries over:
overrides/resolutions,.npmrcregistries and auth tokens,workspace:*links, andpackage.json’s key order and other fields.
5. Runtime compatibility
What we checked beyond the frameworks:
| Works | Doesn’t work yet |
|---|---|
CommonJS and ESM, require("node:*") | Top-level await returned the Promise on 2.9.0 (fixed in 2.12.1) |
TypeScript with no config (types stripped, not checked) — but not tsc itself | npm:, jsr: and https:// import specifiers |
http, fs, path, child_process, crypto hashes and HMAC | pbkdf2Sync().toString("hex") and WebCrypto importKey("raw") for HMAC/PBKDF2 returned wrong results on 2.9.0 (fixed in 2.12.1) |
fetch, Request/Response, WebSocket, web streams, URLPattern, structuredClone | fs.promises.opendir, Module._nodeModulePaths, module.enableCompileCache |
node:sqlite | Deno.*, Bun.* globals |
If a dependency fails with a TypeError: … is not a function on a Node
API, that API is probably missing.
6. Tests
3va testcovers Jest’s core API:describe/test/expect, common matchers,beforeEach,toMatchSnapshot.- Snapshots are stored as
.snap.json, and existing Jest.snapfiles are not reused. jest.config.js, presets (jest-expo,ts-jest),jest.mockof modules and custom environments (jsdom) aren’t part of 3va’s runner. Keep those suites on Jest.- Tests run with permissions too. In our checks, file reads and writes
inside the project worked, but env vars and child processes were
denied. Grant them in
package.json.
7. CI and deployment
- Install 3va in CI from a pinned release and check the binary’s
SHA-256 (
3va --versionprints it). - Containers: use
3va start --attachas the entrypoint so the supervisor is PID 1. - Absolute paths: workspace symlinks are absolute, so run
3va installinside the image, not on the host. - Logs: 3va 2.9.0 printed debug lines to stderr (
[3va-requireFrom],[3va-tcp],[heartbeat]). The first two are gone in 2.12.0 and the[heartbeat]line (every 10 s) is gone in 2.12.1; on older versions filter them out of alerting.
8. Rollback plan
Keep the way back cheap:
- Keep your original lockfile (
package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lock) committed until the migration is done. - Keep the
"3va"permissions block. Node, npm, pnpm, Yarn and Bun ignore it. - Keep scripts that call
nodealongside the3vaones ("start": "node server.js","start:3va": "3va run server.js") until production has run on 3va for a while.