Embedding the Growth Lab GUI
The GUI is designed to run inside an <iframe> in your application. Embedding is governed
by a Content-Security-Policy frame-ancestors allowlist configured for your
tenant; your host origin must be registered before the GUI will render inside it.
Available
- The iframe bridge (v1):
lab-ready(re-announced until answered),ping, origin allow-listing, theinithandshake, token exchange throughPOST /v1/auth/session resizemessages, thethemeandmobilehints oninit,layout,select-sources,sources-selected,targets-status, anderrorcodes for a rejected token or a blocked cookie
Planned
- the
localehint oninit
1. Add the iframe
<iframe
id="growth-lab"
src="https://lab.example.com/"
allow="clipboard-write"
style="width:100%;height:100%;border:0"
></iframe>2. Wait for lab-ready, then send init
The GUI posts lab-ready to your origin as soon as it has loaded. Reply with an init
message carrying a session token your backend minted for the current user
(POST /v1/users/{id}/session-tokens, see Authentication). The GUI
never trusts identity claims from the host; it exchanges the token with the Engine, which
issues the session cookie.
Add your message listener before you set the iframe src. The GUI re-announces
lab-ready about twice a second until it gets an init, and you can send ping at any time
to pull a fresh announcement, but after about 15 seconds it stops and shows the user a
“waiting for the host application” message.
const frame = document.getElementById("growth-lab");
const GUI_ORIGIN = "https://lab.example.com";
window.addEventListener("message", (event) => {
if (event.origin !== GUI_ORIGIN) return; // always check the origin
const msg = event.data;
if (msg?.source !== "growth-lab-gui" || msg.v !== 1) return;
if (msg.type === "lab-ready") {
frame.contentWindow.postMessage(
{
source: "growth-lab-host",
v: 1,
type: "init",
credential: { kind: "session-token", token: "<token minted by your backend>" },
theme: "light", // optional UI hints: theme, locale, mobile
mobile: false, // host viewport, do not infer from iframe width
},
GUI_ORIGIN // never "*"
);
}
if (msg.type === "session-state") console.log("authenticated:", msg.authenticated);
});Message reference (v1)
GUI → host
type | Payload | Meaning |
|---|---|---|
lab-ready | (none) | GUI loaded and listening; repeats until init arrives |
session-state | { authenticated: boolean } | Result of the init handshake / later changes |
resize | { height } | Content height in CSS pixels changed |
error | { code, message } | Handshake or runtime error (see codes below) |
sources-selected | { sourceIds: string[] } | The workspace’s current source selection (create/edit draft, or the open flow) |
targets-status | { flow, runId, targets[] } | Latest run colour / trigger / probability / tiles per bound source, for a host map. flow is null and targets is empty when nothing is open |
host → GUI
type | Payload | Meaning |
|---|---|---|
init | { credential: { kind: "session-token", token }, theme?, locale?, mobile? } | Start a session (token is single-use, expires in minutes). theme sets light/dark; mobile is the host viewport (narrow iframe on desktop is not mobile); locale is accepted but not applied yet |
ping | (none) | Ask for an immediate lab-ready |
layout | { mobile } | Host viewport changed (resize / rotate). The GUI stacks Sources list/map only when mobile is true |
select-sources | { sourceIds: string[] } | Bind those sources on the Create/Edit Sources step, or select a flow that already uses them |
Every message includes source (growth-lab-gui / growth-lab-host) and v: 1.
targets-status (host map)
When a flow is open on the Flows tab, the GUI posts the latest per-source run snapshot
so a host map can colour polygons and show the same tile tooltip the Flows tab does.
Only those targets belong on the map; hide the rest of the catalog. Create and edit
clear flow so the host can show every source and use sources-selected for highlight.
if (msg.type === "targets-status") {
// msg.flow → { id, name } or null
// msg.runId
// msg.targets[] → { sourceId, color, triggered, probability, tiles[] }
// tiles[] → { key, row, column, lastValue, isTriggered, reportOnly, error }
}color is the Engine run colour (the flow’s trigger / reset pair, or the probability
gradient when that is enabled). probability is 0–100. Clear your overlay when
flow is null.
error codes
code | Meaning | What to do |
|---|---|---|
session-exchange-failed | The Engine rejected the token (expired, already used, or minted for another tenant) | Mint a fresh token and send init again |
cookie-blocked | The Engine accepted the token but the browser did not keep the session cookie | Serve both the GUI and the Engine over HTTPS; the user can also grant access from the retry button |
init-timeout | No valid init arrived within the handshake window | Check your listener order, the GUI origin, and your allowlist |
Cookies in third-party context
The Engine session cookie is set Secure; HttpOnly; SameSite=None; Partitioned so it works
when the GUI is embedded cross-site. This requires HTTPS on both the GUI and the Engine:
a plain-http deployment cannot set a Secure cookie, so the session silently fails to stick.
After the exchange the GUI verifies the cookie actually round-trips (GET /v1/auth/me) instead
of assuming a 200 means success. If it does not, you get the cookie-blocked error and the
user sees a retry button that asks for storage access first (Safari/Firefox); Chrome relies on
the Partitioned attribute instead and needs no prompt.