> ## 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.

# The iframe bridge

> Drive an embedded preview, and hear back from it, over postMessage.

An embed sits on a different origin from your page, so none of it is reachable
from your JavaScript: no DOM, no sandbox handle, no terminal. The bridge is the
one seam through it; a small versioned `postMessage` protocol you can drive the
preview with and hear back on.

Every message carries `vpod: 1`. That is the protocol version, and it is also what
tells a vpod message apart from everything else posting into a page; several
frameworks post bare strings, so a message without it is ignored in both
directions.

## In one page

```html theme={null}
<iframe id="vpod"
        src="https://preview.vpod.sh/snap_abc123?key=vpod_pk_...&autoboot=1"
        allow="cross-origin-isolated"
        style="width:100%;height:420px;border:0"></iframe>

<script>
  const frame = document.getElementById("vpod");
  const origin = new URL(frame.src).origin;

  // Always an explicit target origin, never "*".
  const send = (message) =>
      frame.contentWindow.postMessage({ vpod: 1, ...message }, origin);

  addEventListener("message", (event) => {
      if (event.origin !== origin) return;
      if (event.data?.vpod !== 1) return;

      switch (event.data.type) {
          case "ready":
              send({ type: "run", command: "uname -a" });
              break;
          case "output":
              console.log(event.data.stream, event.data.chunk);
              break;
          case "exit":
              console.log("exit", event.data.code);
              break;
      }
  });
</script>
```

<Note>
  Wait for `ready` before sending anything you care about. A message posted before
  the preview has attached its listener is not queued, it is lost — and `ready` is
  also the first point at which there is a machine for `run` to run on.
</Note>

## What the preview sends

| Message  | Fields                                 |                                                                                            |
| :------- | :------------------------------------- | :----------------------------------------------------------------------------------------- |
| `ready`  | `snapshot`, `theme`, `network`, `warm` | The machine is up and the prompt is drawn.                                                 |
| `output` | `stream`, `chunk`                      | A chunk of guest output, as it reaches the terminal. `stream` is `"stdout"` or `"stderr"`. |
| `exit`   | `code`                                 | A command finished.                                                                        |
| `error`  | `reason`, `message`                    | Something did not work. `reason` is `"snapshot"` or `"boot"`.                              |

Two fields on `ready` are worth reading rather than ignoring:

* **`network`** is `"sab"` or `"none"`, which is the honest answer to "can the
  guest reach the internet from here" (see
  [networking inside an iframe](/preview/embed#networking-inside-an-iframe)). It
  says nothing about which backend the SDK chose, on purpose.
* **`warm`** is whether the snapshot came from cache rather than the network, so
  a page timing its own demo is not quoting a cold number as a warm one.

`error` distinguishes a snapshot that could not be resolved (`"snapshot"`, raised
before any bytes are downloaded) from a machine that would not start (`"boot"`).

<Warning>
  An embed refused for its origin sends **nothing**. It is a different document
  with no sandbox in it, so the absence of messages is what you get, not an
  `error`. Do not wait on a message to detect a bad origin — if `ready` never
  arrives, check the key's allowlist.
</Warning>

## What the preview accepts

| Message      | Fields    |                                                                                                |
| :----------- | :-------- | :--------------------------------------------------------------------------------------------- |
| `run`        | `command` | Type a command at the prompt and run it. Needs a key — see below.                              |
| `reset`      | —         | Close the machine and go back to the poster. The cache is untouched, so the next boot is warm. |
| `theme`      | `name`    | Switch themes live. One of the [seven names](/preview/embed#parameters).                       |
| `clearCache` | —         | Evict cached snapshots from origin-private storage.                                            |

An unknown theme name is ignored rather than falling back to the default. That is
the opposite of the URL's behaviour, and deliberately: a `?theme=` typo is a page
you can only fix by reloading it, where a message you sent is one you can send
again.

<Warning>
  `clearCache` clears origin-private storage for the **preview origin**, which is
  every snapshot cached there — not only the one in your embed. It is meant for a
  "download it again" control, not for tidying up after a demo.
</Warning>

## `run` needs a key

`run` is honoured for a **keyed** preview and refused for a keyless one.

By the time the message arrives, the browser has already enforced
`frame-ancestors` from the key's allowlist, so "framed at all" *is* "on the
list" — there is no second check to do and no allowlist to ship to the client. A
keyless preview is served `frame-ancestors *`, so being framed vouches for
nothing, and an embed that any page could execute commands in is a different
product.

Everything else works either way. A keyless embed still reports `ready`, streams
`output`, and takes `reset` and `theme`.

## Origins, in both directions

Three rules, and each closes a real hole:

* **The preview always posts to an explicit target origin, never `"*"`.** A
  preview that broadcast guest output to whoever happened to be framing it would
  leak the contents of a private snapshot to a page that only managed to load it.
* **Inbound messages whose `event.origin` is not the embedder are ignored.** The
  embedder is not the only thing that can reach that window.
* **Anything without the `vpod` tag is ignored**, in both directions.

The preview works out who is framing it from `location.ancestorOrigins`, which is
exact and unaffected by `Referrer-Policy`, and falls back to `document.referrer`
where that does not exist (Firefox).

<Note>
  An embedder serving `Referrer-Policy: no-referrer` in Firefox leaves the preview
  with no origin it can address, so it sends no messages at all. The embed still
  loads and still works — it is the bridge that goes quiet. Chromium and Safari have
  `ancestorOrigins` and are unaffected.
</Note>

## A preview you drive and nobody types in

`readonly=1` disables the keyboard at the terminal, but not `run`: the `cmd`
parameter and an embedder's `run` message both call the line editor directly.

```html theme={null}
<iframe src="https://preview.vpod.sh/snap_abc123?key=vpod_pk_...&readonly=1&autoboot=1"></iframe>
```

That combination is a real machine running real commands, driven only by your
page — a guided tour where the buttons are yours, rather than a terminal a
visitor can wander off in.
