# Embed a Space — one line, and the drop-in doesn't cost you the encryption > **Status: live.** The host-side SDK is served at `https://witbitz.chat/embed.js`; the iframe seam ships in the Spaces > app. **Live demo → [witbitz.chat/demo](https://witbitz.chat/demo):** talk to an embedded AI and it calls the *page's own* > `set_site_color` tool to restyle the site — *your app's functions become the agent's tools*. A simpler read-only > event-log demo (the bridge events, no host tools) is at > [`/site/embed-demo.html`](https://witbitz.chat/site/embed-demo.html). Cross-origin, browser-verified. ## The one-liner ```html ``` or drive it from script: ```js const space = Witbitz.embed('#slot', { link }) space.on('message', m => …) // someone posted (metadata only) space.on('turn', t => …) // an agent turn finished space.signIn(token) // owner-gated rooms: admit the user at their tier ``` `Witbitz.embed(target, opts)` returns a controller: `on(type, cb)` / `off`, `signIn(token)`, `navigate(to)`, `setTheme(theme)`, `destroy()`, and `iframe`. `` re-exposes it as `el.space`. ## Why it's an iframe — and why that's the point That one line mounts a **cross-origin iframe on the Spaces origin**, not inline DOM in your page. That isn't a limitation to apologize for — it's what keeps the guarantees true when a Space is embedded: - The **verified** Space client runs in **its own origin**. Your page — and every tracker and third-party script on it — **cannot reach into it**, cannot read the room key, cannot read the plaintext. - If a Space were inlined into your page, your page would become part of its trusted computing base, and *content-blind* would quietly stop being true the moment anyone embedded it. The iframe is the isolation boundary. So a drop-in embed **does not cost you the encryption** — the part that touches your users' content still runs only in verified code, in a context your page can't see. ## The bridge (host ⇄ Space) The only channel across an origin boundary is `window.postMessage`. The SDK layers a small, **origin-checked** protocol (JSON-RPC 2.0) on it. It is a **narrow capability seam, not shared memory** — the host can do exactly these things: | Direction | Message | Meaning | |---|---|---| | Space → host | `ready` | the Space handshaked; the bridge is open | | Space → host | `message` | someone posted — **metadata only** (`{id, self, sys}`), never content | | Space → host | `turn` | an agent turn finished | | Space → host | `member` | a member event | | Space → host | `resize` | content height changed (auto-sizes the iframe) | | host → Space | `signIn` | hand in an owner IdP token / signed grant | | host → Space | `navigate` / `theme` | deep-link / theme within limits | Discipline that makes the seam safe: **origin-checked both directions** (the Space only accepts control from the one validated embedder; the host trusts only the Spaces origin), **nothing is posted to `'*'`**, and **the room key never crosses the bridge** — it rides the iframe's URL fragment, client-side, and is never sent to a server. Sign-in is a **verified token**, not a trust-me flag: it's checked against the room's sealed allow-list and resolved to a tier (see [the owner rule](./identity-and-admission.md#the-owner-rule)). One honesty note: the app that *embeds* a room necessarily holds that room's link, so it holds that room's key — the isolation doesn't hide the embedded room from the embedder. What it protects is (a) the platform-wide guarantee that *Witbitz* can't read content, and (b) the room from your page's *other* scripts and trackers. ## Who may embed — two gates Embedding is **off by default** (`frame-ancestors 'self'`). Turning it on is two layers, deliberately: - **`frame-ancestors` — the app-wide COARSE gate** (browser-enforced). It *has* to be coarse: the browser evaluates the header before any JavaScript runs, when the room isn't even known (especially for `#mk=` fragment links). Set the allowed embed origins for a deployment via `SPACE_EMBED_ORIGINS` (it feeds both this header and the client check from one source, so they can't drift). - **The owner policy — the per-room PRECISE gate** (client-enforced). The owner's **signed** `AppPolicy` may name `embedOrigins` — the origins allowed to embed *that app's* rooms. Because the client holds the room key, it reads the sealed, signed policy itself and **closes the bridge** if this embedder isn't on the list. The server never sees it — content-blindness is preserved. On the hosted multi-tenant platform the owner key is resolved from the tenant registry (`op:'policy'`); on a pinned single-owner deployment it's the baked key. So the browser stops any non-registered origin from framing at all; the owner policy narrows it to *this owner's* origins per room. Fail-closed. ## Orthogonal to what the Space *is* The embed is transport + isolation. Whether the client is *trustworthy code* is a separate axis — [the attested client](./client-architecture.md#the-attested-client); admission and tiers are [the owner rule](./identity-and-admission.md#the-owner-rule). They compose: an embedded Space is still content-blind, still owner-governed, still verifiable. One line: **`witbitz.chat/embed.js` drops a real, isolated, content-blind Space into any page, talks to it over a narrow origin-checked bridge that never carries your plaintext, and lets the room's owner decide who may embed it.**