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.funcan’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 inruntime.embed_originsinmanifest.userland.json, for example["https://www.example.com"], and publish. Each entry ishttps://, 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. Ifuserland validatewarnsschema_strictaboutruntime.embed_origins, the CLI predates the setting: publishing still works, but--strictfails 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
- Each entry is
https://followed by a domain name and an optional port:https://www.example.comorhttps://shop.example.com:8443. Letters are stored in lowercase. https://*.example.comcovers every subdomain ofexample.com, such aswww.example.comandeu.shop.example.com, but notexample.comitself. 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, orlocalhost. 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 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.
- Let page loads (requests whose
modeisnavigate) go to the network, or serve copies the worker cached from the network, which keep their headers. Don’t build pages withnew 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.