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: talk to an embedded AI and it calls the page's ownset_site_colortool 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. Cross-origin, browser-verified.
The one-liner
<script src="https://witbitz.chat/embed.js"></script>
<witbitz-space link="https://witbitz.chat/space#<your link>"></witbitz-space>
or drive it from script:
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. <witbitz-space> 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).
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 viaSPACE_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
AppPolicymay nameembedOrigins— 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; admission and tiers are 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.
Machine-readable source: embed.md · every doc in one fetch: llms-full.txt (HTML) · ← tools-widgets · async-spaces →