Skip to Content
Growth Lab GUIEmbedding the GUI

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, the init handshake, token exchange through POST /v1/auth/session
  • resize messages, the theme and mobile hints on init, layout, select-sources, sources-selected, targets-status, and error codes for a rejected token or a blocked cookie

Planned

  • the locale hint on init

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

typePayloadMeaning
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

typePayloadMeaning
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

codeMeaningWhat to do
session-exchange-failedThe Engine rejected the token (expired, already used, or minted for another tenant)Mint a fresh token and send init again
cookie-blockedThe Engine accepted the token but the browser did not keep the session cookieServe both the GUI and the Engine over HTTPS; the user can also grant access from the retry button
init-timeoutNo valid init arrived within the handshake windowCheck 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.