# Previews

> An agent runs your app's dev server in its sandbox, and you open it in your browser while it works.

Some changes are easier to judge by using them: a new checkout flow, a layout on a small screen, a chart with real data. An agent can start your app's dev server inside its run and share it as a preview. You click **Open preview**, and the app opens in a tab of yours, served live from the agent's sandbox while it keeps working.

An agent starts a dev server in its run and exposes port 5173 as a preview called Checkout. You open it from the run page. Ryph checks you can see the run, and the page arrives over a tunnel the run opened. When the agent edits the code, your tab updates.

Nothing in the run is published. The run dials out to Ryph, and Ryph sends your browser through that connection once it has checked you can see the run. Hot reload and WebSockets work as they do on your own machine.

Ask for one in plain words, wherever you talk to the agent:

```
@ryph start the dev server and send me a preview of the new checkout
```

An agent with a [browser](https://ryph.ai/docs/browser.md) can click through the same dev server itself. A preview is how you get to click through it too.

Hot reload, streaming responses and WebSockets all work, so when the agent saves a change, your tab updates as it would on your own machine.

## How a preview comes up

1. **The agent starts the dev server**

   It runs the server with `ryph__background_run` as a service, so the server keeps running while the agent works on, and the agent hears about it if the server stops.

2. **It exposes the port**

   It calls `ryph__expose_port` with the port and a label, such as `{ "port": 5173, "label": "Checkout" }`. Ryph registers the preview and hands back its link, which the agent shares in the thread, the task or the pull request.

3. **You open it**

   Ryph checks you can see the run, then lets your browser through to the dev server. The run opened the connection from inside, so nothing in it had to be published.

4. **It lives as long as the run**

   When the run ends, its previews close. If the dev server crashes, the preview answers as unavailable until the agent starts it again on the same port.

## Where to open it

1. Open the task in the web app.
2. In the transcript, the agent's **Exposed a preview** step has an **Open preview** button.
3. Or switch to the run's **Commands** tab. Every preview of the run is listed under **Previews**, with its label and port, and its own **Open preview** button.

Next to the list, Ryph says who can open the run's previews: **People who can see this run**, or **This machine only** when Ryph runs on your machine.

A link the agent posts in a chat thread or a pull request works too. On a hosted deployment, opening it takes you through Ryph's sign-in first if you need it, then the same check.

## Who can open a preview

On [Kubernetes](https://ryph.ai/docs/kubernetes.md) with a [preview domain](#set-up-a-preview-domain), anyone who can see the run can open its previews, and nobody else. Each time you open one, Ryph checks you can see the run and gives your browser a one-time ticket, good for 60 seconds. The preview trades it for a cookie that works for that one preview, for up to eight hours. [Networked Compose](https://ryph.ai/docs/networked-compose.md) runs the same check, for a browser on the host.

On [your machine](https://ryph.ai/docs/try-locally.md) and with [Docker Compose](https://ryph.ai/docs/docker-compose.md), previews open at `http://<id>.localhost`, which only a browser on that same machine can reach. Anyone on the machine who has the link can use the app.

## How Ryph contains a preview

The agent wrote the code a preview serves, and an agent can be talked into things by what it reads. So on a hosted deployment, Ryph treats every preview as a hostile page and fences it in:

- **Every preview gets its own origin,** `<id>.<your preview domain>`, with an id nobody can guess. The preview domain is separate from your Ryph app, so a page in a preview can't read or use your Ryph session.
- **The viewer cookie works for one preview only.** It's set for that exact host name, so another preview can't read it or overwrite it. Ryph removes it from every request before it reaches the dev server.
- **The page reaches what the run could.** Every response carries a content security policy built from the agent's [network allowlist](https://ryph.ai/docs/egress.md), which holds the page's requests, WebSockets, scripts and forms to the hosts the run may reach.
- **The dev server learns little about you.** Ryph removes your IP address from every request, and drops response headers that would reach past the preview, such as `Clear-Site-Data` and reporting endpoints.
- **Nothing outlives the run.** Ryph refuses to install service workers, so a page can't leave code running in your browser after its run ends.
- **Only ports the agent registers are reachable,** between 1024 and 65535, up to four per run. Ryph's own services inside the run, such as its control API, tool server and browser, can't be exposed.

## What to keep in mind

Some things are out of Ryph's reach once the page is in your browser:

- **A page can send data out by navigating.** A link it opens, a window it pops up, WebRTC and DNS prefetching all get past a content security policy. Assume anything you type into a preview can leave it.
- **A page can look like anything,** including your company's sign-in. Sign in to Ryph only on your Ryph app's address, and don't enter a password into a preview.
- **Access is checked when you open a preview.** Someone who loses access to a run keeps a preview they already opened until their cookie expires, after up to eight hours, or the run ends.
- **Previews can toss cookies at each other.** Page code can set a cookie for the whole preview domain, which every other preview then receives. The worst it can do is confuse another preview, or fill your browser's cookies for the domain until previews stop loading for you, which clearing them fixes. Browsers only keep subdomains apart like this for domains on the [Public Suffix List](https://publicsuffix.org/), which is meant for domains that host many unrelated sites, so a preview domain will usually have this gap.
- **Old Safari is looser.** Safari before 16.4 lacks the request headers Ryph uses to tell a page load from an embedded script, so there one preview can load another's scripts.

> [!WARNING]
> **On your machine, a preview runs with your network**
>
> On the `ryph` binary and Docker Compose, a preview is code the agent wrote, running in your browser with your machine's network. It can reach other services on the machine and your local network, and the internet, even when the run itself is confined. Open previews of runs you trust.

## Getting your dev server ready

Most dev servers work as they are. A few settings trip them up:

- **Listen on localhost inside the run.** The tunnel connects to `127.0.0.1` or `::1` on the exposed port.
- **Allow the preview's host name.** Dev servers that check the `Host` header, such as Vite with `server.allowedHosts`, need the preview's domain on the list: `.localhost` on your machine, or your preview domain on a hosted deployment.
- **Use relative URLs.** An app that hardcodes `localhost:5173`, an OAuth callback URL or a fixed hot reload address has to be configured with the preview's URL. The tunnel doesn't rewrite your code.
- **Call a separate API through the dev server's proxy.** Requests from one preview to another are refused, so put the API behind a same-origin proxy, such as Vite's `server.proxy`.

If your app needs a database or a cache to start, give the repo an [environment](https://ryph.ai/docs/environments.md) with those services, and the dev server finds them as it would anywhere else.

## Previews on your deployment

Previews are most useful when your whole team can open them, which takes a preview domain. Today that's possible on Kubernetes. Everywhere else, a preview is local-only: it opens only in a browser on the machine Ryph runs on.

| Deployment                                    | Who can open previews                                                                              | Where they open                      |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------ |
| [On your machine](https://ryph.ai/docs/try-locally.md)         | Local-only. A browser on the same machine                                                          | `http://<id>.localhost:<port>`       |
| [Docker Compose](https://ryph.ai/docs/docker-compose.md)       | Local-only. A browser on the host                                                                  | `http://<id>.localhost:7490`         |
| [Networked Compose](https://ryph.ai/docs/networked-compose.md) | Local-only. A browser on the host, for people who can see the run                                  | `http://<id>.preview.localhost:7490` |
| [Kubernetes](https://ryph.ai/docs/kubernetes.md)               | Your team: everyone who can see the run, once you [set a preview domain](#set-up-a-preview-domain) | `https://<id>.<your preview domain>` |

**On your machine,** `RYPH_LOCAL_PREVIEWS=off` turns previews off. They're also off when `RYPH_APP_URL` points somewhere other than `localhost`, since a `.localhost` link means nothing to a browser on another machine. The same goes for Docker Compose, where `RYPH_PREVIEW_PORT` moves the published port.

**On Kubernetes,** previews are off until you give them a domain, set up as below.

## Set up a preview domain

On Kubernetes, Ryph serves previews from a dedicated domain: every preview at `https://<id>.<preview domain>`, open to everyone who can see its run. That domain is the boundary between code an agent wrote and everything else your people are signed in to, so choose it with care.

> [!WARNING]
> **Use a dedicated domain that serves nothing but previews**
>
> Register a new domain for previews, such as `acme-previews.dev`, and use it for nothing else. No app, no sign-in, no marketing site, no other subdomains, and no cookies set by anything but Ryph.
>
> Don't use a subdomain of a domain you already use, such as `previews.acme.com`. Every subdomain of `acme.com` is a separate origin, but cookies don't follow origins: a cookie can be set for all of `acme.com`, and browsers treat its subdomains as the same site, so `SameSite` doesn't keep them apart. Page code in a preview could then set cookies that your other `acme.com` apps receive, which can log people out or fix their session to one an attacker chose. And any cookie those apps set for all of `acme.com`, sessions included, would be sent to the agent's dev server with every request.

To set it up:

1. **Register the domain** and keep it for previews alone. It must not be the domain your Ryph app is on, at any level.
2. **Point `*.<domain>` at the cluster** with a wildcard DNS record and a wildcard certificate, through `ingress.previews` or `httpRoute.previews` in the Helm chart.
3. **Turn previews on** in the chart's values:

   ```yaml
   previews:
     enabled: true
     domain: acme-previews.dev
   ```

4. **`RYPH_PREVIEWS_TOKEN`** is generated by the chart. To bring your own, set it under `secrets.internal`, 32 random characters or more. Ryph signs preview addresses and viewer cookies with it, so anyone holding it can let themselves into any preview. Changing it gives every preview a new address and signs everyone out of open ones.

The service's internal port, `7491`, carries runs' tunnels and Ryph's own requests. Route only the public port, `7490`, to the internet. The chart's network policies already keep `7491` to the control plane and runs.
