> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vpod.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Running in a browser

> Serve the SDK from a page, and what the guest can reach once it boots.

The same package that runs under Node runs in a browser tab, with no server and
no native dependency. [browser.vpod.sh](https://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:

```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```

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:

```ts theme={null}
import { Sandbox } from "@capsule-run/vpod/browser";
```

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:

```ts theme={null}
const sandbox = await Sandbox.create({
    workerUrl: "/vpod/worker/entry.js",
    componentUrl: "/vpod/component/vpod.js",
    networkWorkerUrl: "/vpod/net/entry.js",
});
```

<Note>
  Wire the copy to a `prebuild` script. Done by hand, the next upgrade leaves an old
  engine behind a new API.
</Note>

If you have nowhere to serve those files from, because your host loads everything
from blob URLs, see [Embedding](/sdk/typescript/embedding) instead.

## What the network can reach

`sandbox.network` reports the backend in use and what it implies:

|                       | `sockets` (Node) | `fetch` (browser) | `none` |
| :-------------------- | :--------------- | :---------------- | :----- |
| Raw TCP               | yes              | no                | no     |
| Arbitrary ports       | yes              | 80 and 443 only   | no     |
| Subject to CORS       | no               | yes               | no     |
| Byte-faithful headers | yes              | no                | no     |
| UDP                   | yes              | no                | no     |

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.

```ts theme={null}
import { explainUnreachable } from "@capsule-run/vpod/browser";

const reason = explainUnreachable(sandbox.network, 22);
// "port 22 is unreachable on the fetch backend, which serves only 80 and 443."
```

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:

```ts theme={null}
const sandbox = await Sandbox.create({
    corsProxy: "https://my-proxy.example.workers.dev",
});
```

## 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](/sdk/typescript/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.

<Note>
  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.
</Note>

## 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](/sdk/typescript/sandbox#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.
