Skip to main content

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 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.
  • Hand the embed its token by message instead of in ?jwt=, so the token never appears in a URL — see Token handoff.
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.
This applies to embeds of /share/application/:id, and of /share/dashboard/:id, which forwards to it. Filters are expressed as URL query params — see URL Filter Parameters for the f_ omnibar param format.

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. 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:
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 "*".
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. To keep the token out of the iframe’s own URL as well — and so out of every access log between the browser and Upsolve — use Token handoff.

Token handoff

A ?jwt= in the iframe src is part of the URL, so it is written to every access log on the way — your load balancer’s, a proxy’s, the web server’s — on every page load. With token handoff, the iframe asks your page for the token over postMessage instead, and the token never appears in a URL at all. Open the iframe with tokenHandoff=true and no jwt:
The iframe then posts embed:ready with awaitingToken: true. Reply with the token:
(getUpsolveToken stands for a call to your backend, which gets the token from POST /v1/api/projects/embed-token.)
  • Refreshing the token. Post embed:set-token again whenever you have a fresh token — before the old one expires — and the embed switches to it in place. The dashboard stays on screen; its charts reload under the new token. The newest message wins, so send a refreshed token only after any earlier reply has gone.
  • Only your page is trusted. The iframe accepts embed:set-token only from the declared parentOrigin, and only from its own parent window — not from another frame on the same origin.
  • The token stays in memory. The iframe never writes it to its URL or to browser storage, and never posts it back. /share/dashboard/:id hands the token over when it forwards to /share/application/:id, still without a URL.
  • If no token arrives. The iframe asks again every second and, after 15 seconds without an answer, shows No token received from the embedding page. A token that arrives later still loads the dashboard.
  • parentOrigin is required. Without a valid one, or outside an iframe, there is nothing the embed would accept a token from, so it says so immediately.
  • ?jwt= still works. If the src carries a jwt, the iframe uses it and does not ask for a token.
  • Token handoff works for /share/dashboard/:id, /share/application/:id and /share/workspace/:projectId/:workspaceId (the AI chat), where a refreshed token is taken without interrupting the conversation.

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.
Apply what you receive as the iframe’s height. That is the whole host side:
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.

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.

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:
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 apply.
  • Auth tokens never round-trip through embed:params. Pass the token in the iframe src as jwt, or — to keep it out of URLs entirely — with Token handoff. Either way the iframe never posts it back.