Skip to main content
The same package that runs under Node runs in a browser tab, with no server and no native dependency. browser.vpod.sh is this page’s contents as a working machine, if you would rather see it than read about it.

Serving the page

Two headers, and only if you want the guest to have network:
Without them the page gets no SharedArrayBuffer, and without that the guest cannot receive bytes while it is running. The sandbox still boots and still runs commands. It just has no network. That is the whole setup for a page that loads the SDK as plain ES modules. It finds its own worker and wasm, and Sandbox.create() needs no arguments.

If you build with a bundler

Import the browser entry by name, so the bundler cannot pick the Node build:
Then copy the installed package’s dist/ into whatever your app serves statically, usually public/, and name the three files the SDK fetches at runtime. A bundler cannot emit them for you, and the address the SDK would otherwise work out for itself does not survive the build:
Wire the copy to a prebuild script. Done by hand, the next upgrade leaves an old engine behind a new API.
If you have nowhere to serve those files from, because your host loads everything from blob URLs, see Embedding instead.

What the network can reach

sandbox.network reports the backend in use and what it implies: In a browser every request the guest makes leaves as a fetch from your page, so the browser’s rules apply to it. A host that sends no access-control-allow-origin comes back to the guest as a 502, which is correct behaviour rather than a bug to engineer around.
In practice uv pip install works because PyPI is CORS-open, and apk add does not because Alpine’s mirrors are not. Anything you control can be made reachable by sending the header. For everything else, there is corsProxy.

Reaching hosts that do not send CORS headers

Most of the web sends no access-control-allow-origin, which puts curl targets, git clone and Alpine’s mirrors out of reach. Pointing the guest’s traffic at a relay you run brings them back:

Caching

Two separate caches make the second visit fast, and neither needs configuration. The snapshot lands in origin-private storage on first boot, so later visits skip the download entirely. See Snapshots for inspecting and clearing it. The 25 MB engine is compiled once and kept in the browser’s own code cache, keyed to the URL it was fetched from, so a returning visitor reuses the compiled module instead of recompiling it. Measured in Chrome against a production build, fetching and compiling the engine goes from 527ms on a cold profile to 64ms on a later visit. Nothing needs configuring for that. Cache-Control: public, max-age=0 with an ETag, which is what most static file servers already send, is enough: the conditional request comes back 304 and the compiled module stays attached to the cache entry.
Origin-private storage is site data, not the HTTP cache. Clearing cached images and files in the browser’s settings does nothing to it, which trips up anyone trying to force a fresh download by hand.

Limits

These follow from the browser, not from vpod:
  • No streaming. commands.run() resolves once the command has finished and returns all of its output. There is no byte stream to pipe into a terminal, so anything needing a live tty (vim, top, a bare python REPL) will sit there until you stop it. You no longer have to wait out its timeout to do that: see Interrupting a command.
  • No raw sockets. SSH, databases on custom ports, and anything not speaking HTTP over 80 or 443 are out of reach.
  • CORS decides which hosts answer. Covered above, along with the corsProxy that gets around it.