> ## 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 (iframe postMessage)

> Two-way sync of dashboard filter state 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.

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. |
| 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>

## 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`.
