# Embed Your App

> Let your own website show an app inside its pages, and keep every other site out.

- Canonical page: https://docs.userland.fun/guides/embedding/
- Docs index for agents: https://docs.userland.fun/llms.txt

> **For agents:** Apps on `*.apps.userland.fun` can't be shown in a frame on any other site by default. To let the customer's own website embed the app in an `<iframe>`, list that site in `runtime.embed_origins` in `manifest.userland.json`, for example `["https://www.example.com"]`, and publish. Each entry is `https://`, a domain name, and an optional port, optionally starting with `*.` to cover subdomains. No paths, at most 20 entries, and no Userland addresses (other apps, slugs, `userland.fun`, docs, the console, the API). Inside the frame the app runs signed out, so embed public pages and link to the app for anything that needs sign-in. If the app has a service worker, let it send page loads to the network so pages keep these headers. If `userland validate` warns `schema_strict` about `runtime.embed_origins`, the CLI predates the setting: publishing still works, but `--strict` fails until the CLI is updated. Custom domains are not affected.

## What happens by default

Other websites can't show your app's `*.apps.userland.fun` pages inside their own. If a page on another site, or another Userland app, puts your app in an `<iframe>`, the browser shows an empty or blocked frame instead. Pages on the same app address can still show each other in frames.

Apps on `*.apps.userland.fun` share a domain with other people's apps, and browsers treat them all as one site. Without this rule, another app could load your app in an invisible frame and trick a signed-in visitor into clicking its buttons.

Every response Userland sends from the app's `https://<app_id>.apps.userland.fun/` address and its slugs, including static files, server responses, errors, and `/_userland/*` routes, carries:

```text
Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: SAMEORIGIN
```

## Let your website embed the app

Add the sites that may embed the app to `runtime.embed_origins`:

```json
{
  "app": { "name": "Table Booking" },
  "runtime": {
    "static_root": "public",
    "fallback": "index.html",
    "embed_origins": ["https://www.example.com", "https://*.example.com"]
  }
}
```

Then publish:

```bash
userland validate .
userland apps publish .
```

CLI 0.8.1 and later check `embed_origins` with the same rules as the API. If `userland validate` warns `schema_strict` that `runtime.embed_origins` is not an allowed key, your CLI is older than 0.8.1. The warning is safe to ignore and publishing works, but `userland validate . --strict` fails on it until you run `npm install -g @userland.fun/cli@latest`.

Once the release is live, your website can show the app:

```html
<iframe
  src="https://table-booking.apps.userland.fun/"
  title="Book a table"
  width="100%"
  height="640"
  style="border: 0"
></iframe>
```

The app's responses now carry the sites you listed, and no `X-Frame-Options` (it can't list sites, and browsers that read `frame-ancestors` ignore it):

```text
Content-Security-Policy: frame-ancestors 'self' https://www.example.com https://*.example.com
```

Check it with `curl -sI https://<app_id>.apps.userland.fun/ | grep -i -E 'content-security-policy|x-frame-options'`.

## Rules for embed_origins

- Each entry is `https://` followed by a domain name and an optional port: `https://www.example.com` or `https://shop.example.com:8443`. Letters are stored in lowercase.
- `https://*.example.com` covers every subdomain of `example.com`, such as `www.example.com` and `eu.shop.example.com`, but not `example.com` itself. List both if you need both.
- No paths, queries, fragments, or trailing slashes (`https://www.example.com/`), and no spaces, quotes, commas, or semicolons.
- No `*`, `'self'`, `http://` addresses, IP addresses, or `localhost`. The app's own pages are always allowed, so you never list the app itself.
- At most 20 entries.
- No Userland addresses: other apps, slugs (including your own app's), `userland.fun`, docs, the console, or the API. Allowing one would bring back the problem this rule prevents.

A publish with an invalid entry is refused with `400 invalid_runtime_manifest`, and the message names the entry, for example `runtime.embed_origins[0] ("https://www.example.com/") must be an origin without a path, query or fragment (no trailing slash), ...`.

The list belongs to the release. Publishing a release without `embed_origins` takes the sites away again, and a [rollback](/guides/rollback/) brings back the list of the release you roll back to.

A browser checks every page the frame sits in, so a site you list can show your app, but a page that puts that site in its own frame can't reach your app through it.

## Signed out inside the frame

Browsers don't send your app's sign-in cookie to a page framed by another site, and don't keep one set there, so inside the frame your app runs as a signed-out visitor. The sign-in pages under `/_userland/auth/*` never show inside a frame at all. Embed public pages such as a booking form, a menu, or a price calculator. For anything that needs sign-in, link to the app so it opens in its own tab:

```html
<a href="https://table-booking.apps.userland.fun/account" target="_blank" rel="noopener">Manage your booking</a>
```

## Your app's own headers

If your server code sends its own `Content-Security-Policy` with `frame-ancestors`, browsers apply both policies, so a site must be allowed by both. Use `embed_origins` to allow a site; your own header can only narrow the list. An `X-Frame-Options` header your server code sets is kept as it is.

## Service workers

Browsers check the frame headers on the page they actually show. If your app registers a service worker, a page it answers from its own cache keeps the headers it was stored with, and a page it builds itself has only the headers the worker gives it. Such a page can end up in another app's frame, where it would load signed in.

- Let page loads (requests whose `mode` is `navigate`) go to the network, or serve copies the worker cached from the network, which keep their headers. Don't build pages with `new Response(...)` in the worker.
- Pages cached before Userland added these headers, or before you last changed `embed_origins`, still carry the old ones. Change the worker's cache version so they're fetched again.

## Custom domains

Custom domains are not changed: Userland adds no frame headers on them. A custom domain is a site of its own, so browsers already keep your app's sign-in cookie out of frames on other sites. To keep other sites from framing an app on a custom domain, send your own `Content-Security-Policy: frame-ancestors 'self'` header from server code.
