3va

> Veni, Vidi, Vici, Abiit — He came, he saw, he conquered, he left.
3va is a JavaScript and TypeScript runtime written in Rust with deny-by-default security. It bundles a package manager, process manager, test runner, bundler, and dev server — no pm2, no separate build tool, no extra config.
📖 Full documentation · 🎧 Resumen en audio (Español) vía NotebookLM · 🎧 Audio overview (English) via NotebookLM
Philosophy
The JavaScript ecosystem has a supply chain security problem. Post-install scripts run arbitrary code at install time. Packages silently access the filesystem and network. There is no enforced boundary between trusted application code and untrusted dependencies.
3va starts from a different premise: deny everything, grant explicitly. The design draws from QubesOS, WASI, and the Chrome sandbox — not from Node.js. Every capability (filesystem, network, env vars, child processes, native addons) is blocked by default and must be declared at the command line. This applies uniformly to application code and to every dependency it pulls in.
Quick Start
git clone https://github.com/OdinoCano/3va.git
cd 3va
cargo build --release
sudo cp target/release/3va /usr/local/bin/
# Run a script (no permissions granted by default)
3va run app.ts # or: 3va r app.ts
# Grant specific capabilities
3va run app.ts --allow-net=api.example.com --allow-read=./data
# Install packages
3va install express --allow-net=registry.npmjs.org # or: 3va i express …
# Audit the dependency tree
3va audit --secrets
# Start as a background daemon
3va start app.js --name my-api
> Common aliases: r=run · i/add=install · t/spec=test · d=dev · b=bundle · ws=workspace · sh/shell=sandbox
>
> Output: 3va run only prints your script's own output. Pass -v / --verbose to also see runtime status messages.
For install methods on your platform (npm, Cargo, Scoop, Chocolatey, winget, Snap, Nix, prebuilt binaries), see Installation below.
Comparison
All numbers in this table are generated by CI, not hand-edited — .github/workflows/benchmark.yml runs bench/run.sh on a GitHub-hosted runner (4 vCPU, 15 GiB RAM), builds all three runtimes in release mode (cargo build --release) in the same run, and publishes the table straight to the job summary. The figures below come from the latest run of that workflow on main (all runs) and are independently reproducible — run bash bench/run.sh yourself (see bench/README.md for methodology and caveats). That run measures main, not the 2.11.0 release, so the 3va column includes changes that haven't shipped yet. GitHub's shared runners are weaker and noisier than dedicated hardware: absolute numbers vary from run to run, and a difference of a few percent between two runtimes (plain HTTP below) is within that noise. This table supersedes any other performance numbers elsewhere in this document or its history — treat it as the single source of truth.
| Feature | Node.js 24 | Bun 1.3 | 3va 2.12.0 |
|---|
| JavaScript runtime | ✓ | ✓ | ✓ |
| TypeScript (no config) | ✗ | ✓ | ✓ |
| Package manager | via npm | ✓ | ✓ |
| Process manager | via pm2 | ✗ | ✓ built-in |
| Test runner | via Jest | ✓ | ✓ Jest-compatible |
| Bundler | via webpack/esbuild | ✓ | ✓ |
| Dev server + HMR | ✗ | ✓ | ✓ |
| Deny-by-default permissions | ✗ | ✗ | ✓ |
| Post-install scripts blocked | soon¹ | partial² | ✓ always, no allowlist |
| Permission grants scoped per-dependency | ✗ | ✗ | ✓ |
| Malware + secrets audit | ✗ | ✗ | ✓ |
| OSV CVE scan (24h cache) | ✗ | ✗ | ✓ |
CDP Debugger (--inspect) | ✓ | ✗ | ✓ |
| NAPI native addons | ✓ | ✓ | ✓ |
| WebAssembly (WASI) | partial | ✓ | ✓ |
| Post-quantum TLS (hybrid, RFC 10024) | ✗ | ✗ | ✓ client-only |
Node.js API compat (fs/http/stream/…) | ✓ (is Node) | high, self-reported | ~97%⁴, self-reported |
| ECMAScript (test262) conformance⁷ | ~100%, self-reported (is V8) | ~99.9%, self-reported (JavaScriptCore) | 96.9% (95461/98562, 0 skipped) |
| Platforms | Linux/macOS/Windows/Android | Linux/macOS/Windows | Linux/macOS/Windows⁵ |
| License | MIT | MIT | MIT |
| First release → latest | 2009 → 24.x | 2023 → 1.3 | v1.0.0 → v2.12.0 |
| Startup time (hello world) | 27.3 ms | 4.3 ms | 16.4 ms |
| HTTP throughput (100k req, c=1000) | 20,821 req/s | 73,508 req/s | 69,658 req/s³ |
| Express 5 (100k req, c=1000) | 6,716 req/s | 20,517 req/s | 20,873 req/s |
| Express 5 over HTTPS (100k req, c=1000) | 6,885 req/s | 15,447 req/s | 21,668 req/s |
| Memory, idle → under load | 49.9 → 88.3 MB | 19.5 → 37.9 MB | 43.8 → 77.1 MB |
| Install time (warm cache)⁶ | 424.1 ms | 2.5 ms | 5.5 ms |
¹ npm v12 (shipping July 2026) disables install scripts by default; npm 11 and earlier run them.
² Bun's trustedDependencies allowlist skips scripts for untrusted packages, but a non-registry specifier (file:/git:/github:) sharing a trusted package's name could bypass it (CVE-2026-24910, fixed in 1.3.5).
³ Measured against 3va's HTTP server with its firewall's per-IP connection/rate limits raised — at the shipped defaults (100 req/s, 50 connections per source IP) a single-machine load test like this one is mostly rejected with 403, which is the firewall working as intended, not a bug. See HTTP Performance and Load Behavior for what happens at default limits.
⁴ Per-module breakdown in docs/12-roadmap/04-compatibility.md — self-reported, not measured against an exhaustive conformance suite.
⁵ Android arm64 currently broken on main; see Platform Support.
⁶ Re-running is-odd install when it's already in node_modules (npm: npm install --silent, Bun: bun install, 3va: 3va install --allow-net=registry.npmjs.org) — same package, same warm-cache condition, same CI runner, for all three. bench/run.sh now scripts all three; the npm/Bun columns fill in on the next CI run of .github/workflows/benchmark.yml (Node and Bun are already installed there).
⁷ tc39/test262, the official ECMA-262 conformance suite. 3va's number is the only one of the three actually measured by this repo's CI (crates/test/src/test262.rs, all ~53k tests across language/, built-ins/, and intl402/ — see docs/09-testing/06-test262.md for the category breakdown and known gaps: module-record-only semantics needing native v8::Module, $262.agent/createRealm edge cases, and ICU/CLDR data-version gaps in intl402/). Node's and Bun's figures are their own public self-reported numbers (V8's and JavaScriptCore's own test262 CI, respectively) — not run by this repo, so not held to the "CI-measured" bar the rest of this table is.
Where Bun wins: startup latency (3.1 ms vs 3va's 11.6 ms on the same CI runner), memory footprint (37.8 vs 76.1 MB under load) and the warm no-op install (1.7 vs 3.9 ms). 3va trades some of this for security guarantees Bun's allowlist model doesn't provide (see the CVE above). Startup in particular is not understood yet: on a dedicated 32-core desktop it is level with Bun (7.4 vs 7.3 ms), and pinning both runtimes to 4 CPUs there does not reproduce the CI gap, so core count alone doesn't explain it.
Where 3va wins: permission grants scoped to the dependency that requested them, not the whole process; an install path with no allowlist to spoof in the first place; and throughput under a framework — Express 5 (71,881 vs 48,841 req/s) and Express 5 over HTTPS (69,764 vs 43,502 req/s). Plain HTTP is level with Bun (121,871 vs 116,809 req/s, within run-to-run noise). These come from the event-driven HTTP dispatch and the real-TLS https.createServer merged after 2.11.0, and the dedicated 32-core runs in bench/README.md show the same ordering.
Installation
> Recommended: if you already have Node's ecosystem tooling, npm install -g @edge_166/3va is the fastest path. If you work primarily in Rust, cargo install vvva_cli avoids depending on npm at all.
npm install -g @edge_166/3va
cargo install vvva_cli
Installs a 3va binary on $PATH — the crate is named vvva_cli (not 3va) because crates.io rejects package names starting with a digit.
Scoop (Windows)
scoop bucket add 3va https://github.com/OdinoCano/3va
scoop install 3va
Chocolatey (Windows)
choco install 3va
winget (Windows)
winget install OdinoCano.3va
Snap (Linux)
snap install vvva-cli
Build locally from source (for testing):
snapcraft --use-lxd # uses dist/snap/snapcraft.yaml
sudo snap install ./vvva-cli_*.snap --dangerous
Nix
The flake lives under dist/nix/:
nix run "github:OdinoCano/3va?dir=dist/nix"
Build from source
Requires Rust 1.85+ (stable, via rustup), a C/C++ toolchain, and network access on the first build to download the prebuilt V8 library. FIPS builds also need Go and CMake. See CONTRIBUTING.md § Prerequisites.
git clone https://github.com/OdinoCano/3va.git
cd 3va
cargo build --release
sudo cp target/release/3va /usr/local/bin/
Direct binary download
Pre-built binaries for every platform are attached to each GitHub Release:
| Platform | File |
|---|
| Linux x64 | 3va-vX.Y.Z-x86_64-unknown-linux-gnu.tar.gz |
| Linux arm64 | 3va-vX.Y.Z-aarch64-unknown-linux-gnu.tar.gz |
| macOS x64 | 3va-vX.Y.Z-x86_64-apple-darwin.tar.gz |
| macOS arm64 (M-series) | 3va-vX.Y.Z-aarch64-apple-darwin.tar.gz |
| Windows x64 | 3va-vX.Y.Z-x86_64-pc-windows-msvc.zip |
Permissions
Every capability is blocked by default. Permissions are granted per-invocation via flags and apply equally to application code and all loaded dependencies.
3va run app.ts \
--allow-read=/app/config \ # filesystem read, scoped to a path
--allow-write=/tmp \ # filesystem write, scoped to a path
--allow-net=api.stripe.com \ # outbound network, scoped to a host
--allow-env=DATABASE_URL \ # env var access, scoped to a variable
--allow-child-process \ # spawn child processes
--allow-ffi=./build/addon.node # load native addon (NAPI)
Omitting a flag means the capability is blocked and cannot be enabled from inside the script. In an attended terminal (stderr is a TTY), the runtime asks interactively at the point of first access; in CI, pipes, or redirected output, ungranted capabilities are denied silently.
Unencrypted protocols (http://, ws://, and FTP/IMAP/POP3/IRC/MQTT/gRPC without TLS) are refused for any non-loopback host, even one granted with --allow-net, unless you also pass --allow-insecure. TLS connections require TLS 1.2 or newer and always verify certificates.
Permission scopes can be widened to cover all values:
3va run app.ts --allow-read --allow-net # unrestricted read + network
You don't have to read the package.json format below, or hand-write a single --allow-* flag. One command does it for you:
3va permissions learn app.ts
This actually runs your script (with everything open, so it works end-to-end once), watches what it touches, and hands you back the exact flags to lock it down — no guessing, no trial-and-error loop of "run it, see what got denied, add a flag, repeat":
$ 3va permissions learn app.ts
Running 'app.ts' with all permissions to observe usage...
[... your script's normal output ...]
Observed usage — suggested package.json permissions:
{
"3va": {
"permissions": {
".": {
"allow-net": ["api.example.com"],
"allow-read": ["./app.ts"],
"allow-write": ["./output.txt"],
"allow-env": ["NODE_ENV"]
}
}
}
}
Equivalent CLI flags:
3va run app.ts --allow-net=api.example.com --allow-read=./app.ts --allow-write=./output.txt --allow-env=NODE_ENV
Copy the last line. Done — that's the minimum permission set your script actually needs, not a guess.
To skip the copy-paste, add --write and the observed grants go straight into package.json under 3va.permissions, ready to commit and review:
3va permissions learn --write app.ts
Existing grants are merged, never replaced — running learn again after a refactor cannot drop a permission some other entry point still needs, and per-package scopes ("express": { ... }) are left untouched. Only what the script actually did is recorded; nothing is widened on its behalf.
If you'd rather not execute anything yet (e.g. reviewing an unfamiliar script before running it), 3va permissions suggest app.ts does the same job by reading the source instead of running it — faster and safer to run blind, but a starting point rather than the final word, since it can't see what a conditional branch or dynamic require() would touch at runtime.
The package.json section further down is for teams who want the grants checked into version control instead of typed on the command line every time — worth doing once a project stabilizes, not something you need on day one.
When something is denied
A denied capability no longer only surfaces as whatever error the built-in happened to raise. Every run ends by reporting what was refused and the flag that would allow it:
$ 3va run app.ts
[!] 2 permissions were denied during this run:
read /etc/hosts (denied 2x)
grant with: --allow-read=/etc/hosts
connect to api.example.com:8443
grant with: --allow-net=api.example.com:8443
Run `3va permissions learn` to record the script's real requirements in package.json instead of passing flags each time.
--trace-denials prints each refusal the moment it happens instead, which is what you want when the summary can't tell you which call was refused — a retry loop hitting the same wall 400 times looks identical to one incidental read. A host grant may name a port: --allow-net=api.example.com:443 allows that service and nothing else on the host, while a portless --allow-net=api.example.com still covers every port.
Package-level permission declarations
Besides CLI flags, 3va run reads permission grants from package.json under a "3va" key — a pattern already established by "jest", "eslint", and "prettier". Node.js, Bun, pnpm, and Yarn ignore unknown keys, so there is no conflict. CLI flags and package.json grants are merged (CLI only adds, never revokes a package.json grant).
{
"name": "my-app",
"dependencies": {
"express": "^4.18.0",
"axios": "^1.6.0"
},
"3va": {
"no-prompt": true,
"permissions": {
".": {
"allow-net": ["api.example.com"],
"allow-read": ["./config", "${NODE_MODULES_ROOT}/express@4.22.2"],
"deny-read": ["${NODE_MODULES_ROOT}/express@4.22.2/node_modules/express/lib/express.js"]
},
"axios": {
"allow-net": ["api.example.com"]
},
"express": {
"allow-net": ["*"]
}
}
}
}
- Scope keys are enforced, not just documentation:
. grants apply globally (the app's own code), while a package name like axios or express only applies while that package's own code is executing — a dependency you didn't name gets none of it, even if it's also calling fs/net from the same process.
deny-* fields win over any broader allow-*, letting you grant a vendored directory by prefix while excluding one file with a known CVE.
- Relative paths resolve against the directory containing
package.json, not the invocation cwd — the same file works from any working directory.
${VAR} in path fields expands against the environment of the host process running 3va run (evaluated before any capability exists), so one absolute root that differs per server/team (/var/node_module, /local/bin/node_modules, …) can be declared once and switched per environment without editing package.json. An undefined variable is left as a literal placeholder (fails closed) rather than collapsing to an empty string.
"no-prompt": true is equivalent to passing --no-prompt on every invocation: any capability not covered by allow-* is denied silently instead of prompting.
3va permissions suggest (available today) will be extended to generate this section directly, and 3va permissions learn to persist the observed permission set into it. Full reference: docs/06-permissions/06-package-json-permissions.md.
Package Manager
3va install axios --allow-net=registry.npmjs.org # from npm
3va install react@18 --allow-net=registry.npmjs.org # specific version
3va install @std/path --allow-net=jsr.io # from JSR
3va reinstall axios --allow-net=registry.npmjs.org # force-reinstall one package
3va update --allow-net=registry.npmjs.org # update to latest versions
Post-install scripts are never executed. There are no exceptions. This is enforced at the package manager level, not as a flag.
Package storage uses global deduplication (same model as pnpm): a package at version X is stored once on disk; multiple projects reference it and declare their own permissions independently.
For install-time performance (3va vs. npm vs. Bun, same package, same warm-cache condition), see the Comparison table above — those CI-measured numbers are authoritative; treat any other install-timing figure you find elsewhere (older commits, issues, etc.) as stale.
Audit
3va audit # malware scan + OSV CVE scan (24h cache)
3va audit --secrets # also scan for leaked credentials
3va audit --deny # exit non-zero on CRITICAL/HIGH findings
3va audit --json # machine-readable output
Audit runs in three phases:
- Malware scan — static analysis of
node_modules for known malicious patterns
- OSV CVE scan — queries
api.osv.dev for known vulnerabilities; results cached 24 hours
- Secrets detection (opt-in via
--secrets) — 21 patterns covering AWS keys, GitHub tokens, Stripe keys, private certificates, JWT secrets, database connection strings, and more, scanned over the current project's source files
Process Manager
Built into the runtime. No pm2, no separate daemon process.
3va start server.js --name api
3va start server.js --name worker -- --port 4000 # pass args after --
3va start server.js --name one-shot --no-autorestart # never respawn on crash
3va status # all processes
3va status api # one process
3va logs api --lines 200
3va restart api
3va stop api # SIGTERM → SIGKILL after 1.5s
3va delete api # stop + remove logs
Process metadata and logs live in ~/.3va/processes/. When a managed process dies unexpectedly, the supervisor restarts it automatically with exponential backoff (500 ms → 1 s → 2 s → … → capped at 30 s), giving up after 15 consecutive crashes (tunable with --max-restarts). 3va stop always stops the process for good — it never triggers a respawn. The Restarts column in 3va status counts both automatic and manual restarts.
To disable automatic restarts for a specific process, start it with --no-autorestart; it will then be reported as error until you 3va restart it by hand. A restarted process keeps the exact same permissions it had at first start (same CLI flags, same package.json 3va grants).
The headline throughput number is in the Comparison table (CI-measured, release build, firewall limits raised for the test). The numbers below describe how 3va behaves at its shipped default limits (100 req/s, 50 connections per source IP), which is a deliberate design choice, not a performance regression — a single-machine load test against defaults will see most requests rejected with 403, and that's intentional.
At high concurrency, 3va's connection limiter deliberately sheds excess connections rather than queuing them indefinitely, trading raw throughput for protection against overload. Slowloris and RUDY (R-U-Dead-Yet) protection are built into the HTTP layer (per-line header deadlines, a bodyTimeoutMs body deadline, and a minBodyRateBps minimum body receive rate — see docs/10-security/08-firewall.md). Rate limiting is adaptive: an IP that repeatedly trips the rate limit is auto-blocked, and each repeat offense within the strike window is blocked for longer (blockDurationSecs × blockEscalationFactor^(strikes-1), capped at maxBlockDurationSecs) until the IP stays calm for strikeDecaySecs.
package.json scripts fallback
3va runs package.json.scripts. (via the project's actual package manager — pnpm/yarn/bun/npm, detected by lockfile) whenever `` isn't one of 3va's own subcommands — the same convention npm run /pnpm follow:
3va build # not a 3va subcommand → runs `pnpm run build` (or npm/yarn/bun)
Built-in subcommands always win: 3va dev/3va test/etc. run 3va's own implementation, never a same-named script.
This is not sandboxed — the delegated script is a real external process (the actual package manager, running arbitrary shell), completely outside the permissions system's capability model (which only governs JS executed inside 3va's own V8 engine; internally this lives in the vvva_permissions crate). Consistent with 3va install never running postinstall scripts, running it requires explicit consent: a [y/N] prompt in a TTY, --yes to skip it, "3va": { "no-prompt": true } in package.json to skip it permanently, or a hard deny with no prompt at all outside a TTY (CI, pipes).
Test runner
Jest-compatible. No configuration required.
3va test
3va test tests/unit
3va test --watch
3va test --coverage
3va test --update-snapshots
Supports describe, test, expect, all standard matchers, toMatchSnapshot, watch mode, and coverage reporting. The test runner is implemented in the vvva_test crate — no Jest install needed.
Bundler
3va bundle src/index.ts
3va bundle src/index.ts -o dist/bundle.js --minify
Walks the real import graph from the entry — project files, node_modules (both ESM and CommonJS packages), .json, and .css — and inlines everything into one self-contained file, runnable standalone via 3va run dist/bundle.js or in a browser `