userland Menu

Guides

Embed Your App

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

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:

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:

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

Then publish:

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:

<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):

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

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 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:

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

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.

Userland docsYours, live.