> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upsolve.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Live Filter Sync & Auto-Height (iframe postMessage)

> Two-way sync of dashboard filter state — and content-height reporting — between an embedded Upsolve iframe and your host app

## Overview

When you embed a shared dashboard (`/share/application/:id`) as an iframe, Upsolve
keeps its URL filter state in sync with your host application over the browser
[`postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
API. This lets you:

* **Read** the embedded view's filter state — e.g. mirror it into your own URL so
  the dashboard's filtered state is deep-linkable and survives a refresh.
* **Drive** the embedded view from your own UI — push filter changes down without
  reloading the iframe.
* **Size the iframe to its content** so the dashboard scrolls with your page
  instead of inside a fixed-height box — see [Auto-height](#auto-height).

The iframe emits a message on **every** filter change (the filter omnibar updates
the URL via the History API, and we emit on those updates too — not just full
navigations), accepts filter params you push down, and announces when it is ready
to receive them.

<Note>
  This applies to embeds of `/share/application/:id`. Filters are expressed as
  URL query params — see [URL Filter Parameters](/embedded-bi/filters/url-filter-parameters)
  for the `f_` omnibar param format.
</Note>

## Message schema

Every message has a `type` and a numeric version `v`. Pin to the version you build
against; we bump `v` for any breaking change to a payload shape.

| Direction       | `type`             | Payload          | Meaning                                                                                                                                                                                                                                                     |
| --------------- | ------------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| iframe → parent | `embed:ready`      | `{ v }`          | The iframe's listener is live. Safe to send the initial params.                                                                                                                                                                                             |
| iframe → parent | `embed:params`     | `{ v, search }`  | The iframe's filter state changed. `search` is a query string (leading `?`) containing **only filter params** (`f_*` / `filter_*`) — auth tokens and other internal params are intentionally stripped (see Security). Empty string means no active filters. |
| iframe → parent | `embed:height`     | `{ v, height }`  | The iframe's content height in CSS pixels. Only sent when you opt in with `?autoHeight=true`; see [Auto-height](#auto-height).                                                                                                                              |
| parent → iframe | `embed:set-params` | `{ v?, search }` | Apply these params to the embedded view. `search` may include or omit the leading `?`; an empty string clears all filters. Messages with a `v` newer than the iframe understands are ignored.                                                               |

Current version: **`v: 1`**.

## Security: lock down origins

The iframe never posts to `"*"`. Instead, **you declare your origin** to the iframe
via a `parentOrigin` query param in the iframe `src`:

```
https://ai-hub.upsolve.ai/share/application/{appId}?jwt={token}&parentOrigin=https://your-app.com
```

The iframe validates `parentOrigin` is a well-formed `http(s)` origin, only accepts
messages whose `event.origin` matches it, and only posts back to it. On your side,
**always** validate `event.origin` against the iframe's origin before trusting a
message, and pass an explicit `targetOrigin` when you post — never `"*"`.

<Warning>
  `embed:params` only ever carries **filter params** (`f_*` / `filter_*`). The
  iframe deliberately strips auth tokens (`jwt`, `supabaseToken`, `dbAuthToken`) and
  internal params (`parentOrigin`, `transparent`) before posting, so mirroring
  `embed:params.search` into your own URL bar will never expose an Upsolve token in
  your address bar, browser history, or `Referer` headers. Keep those tokens in the
  iframe `src` you construct — they don't need to round-trip through `postMessage`.
</Warning>

## Auto-height

An iframe has no intrinsic height. It is a replaced element with a spec default
of 300×150 and no content-driven sizing, so `height: auto` does nothing and
`height: 100%` only resolves if every ancestor has a definite height. Whatever
height you pick, the dashboard lays itself out inside it and everything past it
scrolls **inside** the frame: your reader gets a second scrollbar, your page can
never scroll the dashboard as part of its own content, and a print or
full-page screenshot of your page captures only the visible slice.

Add `autoHeight=true` to the iframe `src` and the embed instead:

1. lays out from its content rather than pinning itself to the iframe viewport
   (nothing inside the embed scrolls on its own), and
2. posts `embed:height` with that content height, on load and on every change —
   a chart finishing its load, a filter bar wrapping to a second line, a
   re-layout after you resize the frame.

```
https://ai-hub.upsolve.ai/share/application/{appId}?jwt={token}
  &parentOrigin=https://your-app.com
  &autoHeight=true
```

Apply what you receive as the iframe's height. That is the whole host side:

```js theme={null}
const iframe = document.getElementById("upsolve-embed");

window.addEventListener("message", (e) => {
  if (e.origin !== IFRAME_ORIGIN) return;
  // Multiple embeds on one page: make sure this is the one you are sizing.
  if (e.source !== iframe.contentWindow) return;
  if (e.data?.type !== "embed:height") return;
  iframe.style.height = `${e.data.height}px`;
});
```

<Note>
  `autoHeight` needs `parentOrigin` — as with every other message, the embed will
  not post to `"*"`, so without a declared origin there is nothing to listen for.
</Note>

### Notes

* **Give the frame a starting height anyway.** The first `embed:height` arrives
  after the page mounts; until then the frame is whatever CSS says it is. A
  sensible initial height (or a skeleton) avoids a visible jump from 150px.
* **Nothing scrolls inside the frame.** Auto-height removes the embed's internal
  scroll containers on purpose — with one in place, content that sits outside the
  normal flow ends up in its scroll area rather than in the height we report to
  you, i.e. behind a scrollbar you cannot see. A dashboard wider than the frame
  therefore scrolls the frame's own document horizontally rather than a panel
  inside it; keep the iframe at `width: 100%` and that does not arise.
* **Sticky elements stop being sticky.** The dashboard's filter bar sticks to
  the top of the *scrollport*, and in an auto-height embed there is no scrollport
  inside the frame — your page owns the scrolling. The bar scrolls away with the
  content.
* **Width still comes from you.** Auto-height is height only: keep the iframe at
  `width: 100%` and the dashboard reflows to it.
* **Heights are capped at 20,000px.** A page that somehow sized itself off the
  viewport could otherwise grow a little on every round trip; past the cap the
  embed simply scrolls internally again.
* **Older embeds ignore the param.** If you host a pinned Upsolve version that
  predates this, `autoHeight=true` is inert and no `embed:height` ever arrives —
  so keep whatever height fallback you have today and let a reported height
  supersede it.

## Consumer reference implementation

Drop this into the page that hosts the iframe. It mirrors the iframe's filter state
into your own URL bar, and (optionally) pushes your URL's filters down on load.

```js theme={null}
const IFRAME_ORIGIN = "https://ai-hub.upsolve.ai";

const iframe = document.getElementById("upsolve-embed");

// Build the iframe URL: forward your filters + declare your origin.
iframe.src =
  `${IFRAME_ORIGIN}/share/application/${appId}?jwt=${token}` +
  `${location.search ? "&" + location.search.slice(1) : ""}` +
  `&parentOrigin=${encodeURIComponent(location.origin)}`;

window.addEventListener("message", (e) => {
  // 4. Origin lockdown — only trust the embedded iframe.
  if (e.origin !== IFRAME_ORIGIN) return;

  // 3. Handshake — push the initial params once the iframe is listening.
  if (e.data?.type === "embed:ready") {
    iframe.contentWindow.postMessage(
      { type: "embed:set-params", v: 1, search: location.search },
      IFRAME_ORIGIN
    );
  }

  // 1. The iframe's filters changed — mirror them into your URL bar.
  if (e.data?.type === "embed:params") {
    const url = new URL(location.href);
    url.search = e.data.search;
    history.replaceState(null, "", url); // mirror up without spamming back button
  }
});
```

### Pushing filters live

To change the embedded view's filters from your own UI after load — without
reloading the iframe — post an `embed:set-params` message at any time:

```js theme={null}
function setEmbedFilter(search) {
  iframe.contentWindow.postMessage(
    { type: "embed:set-params", v: 1, search },
    IFRAME_ORIGIN
  );
}

// e.g. filter to a single discipline
setEmbedFilter("f_Raw.olympics_summer.discipline=sm:Athletics");

// e.g. clear all filters
setEmbedFilter("");
```

The dashboard re-renders against the new filters in place.

## Notes & gotchas

* **Send params only after `embed:ready`.** If you post `embed:set-params` before
  the iframe is listening, it is silently dropped. Wait for the handshake.
* **`embed:params` echoes.** After you apply `embed:set-params`, the iframe will
  emit an `embed:params` reflecting the canonical (normalised) query string. The
  reference handler above mirrors it with `history.replaceState`, which does not
  re-emit — so there is no feedback loop.
* **`search` is the source of truth.** Both directions speak the URL query string,
  so the same `f_`/`filter_` param formats documented in
  [URL Filter Parameters](/embedded-bi/filters/url-filter-parameters) apply.
* **Auth tokens stay in the iframe `src`.** Keep `jwt` / `dbAuthToken` in the
  iframe URL you construct; they don't need to be (and shouldn't be) round-tripped
  through `postMessage`.
