Queek docs

Embedded apps and the dashboard bridge

How an app's page runs inside the Queek dashboard. The postMessage contract, exact origins, the handshake and its 5 and 15 second limits, how the frame is sized, and where to put dialogs.

An app's embedded page opens inside the merchant dashboard, in a frame. The page and the dashboard talk through a small postMessage contract. If your page never says it is ready, the merchant sees an error instead of your app, so this contract is the first thing to get right.

@usequeek/app-sdk implements both halves of it for the page. Read this page when you build the contract yourself, when you debug a page that does not load, or before you design its layout.

How the page is framed

The dashboard frames the page at merchant_page_url from the manifest, or at one of its [[extensions.nav]] paths. The frame has an exact sandbox:

allow-scripts allow-forms allow-same-origin

Your page keeps its own origin, so scripts, forms and storage work. The sandbox grants no top-level navigation, no popups and no modal dialogs.

RuleDetail
OriginThe page must be served over https, on an origin that is not the dashboard's own. A loopback origin (localhost, 127.0.0.1, [::1]) is allowed for a development install.
FramingYour server must let the dashboard frame the page: no X-Frame-Options: DENY, and a Content-Security-Policy whose frame-ancestors names https://dashboard.usequeek.com. The review checks warn when the policy is missing.
First loadThe frame address carries queek_token (a session token) and theme (light or dark) in its query string. The SDK reads and removes queek_token and exchanges it with your server.
Popups and dialogswindow.open, alert, confirm and prompt do nothing in the frame. To send the merchant elsewhere, post an open message.

The message contract

Every message is an object with a source and a type. Messages from your page use source: "queek-app". Messages from the dashboard use source: "queek-merchant".

From your page to the dashboard

typeFieldsWhat the dashboard does
readynoneEnds the handshake and answers with token, then theme. Send it on every load, including after a reload inside the frame.
resizeheight: number of pixelsSets the frame height (see How the frame is sized) and answers resize-ack.
navigatedpath: a path that starts with /Mirrors your in-app location in the dashboard address, so reload, back and forward return to the same place.
opentarget: a dashboard path or an https URLOpens a dashboard path in place, or an https URL in a new tab.
acknoneAccepted and ignored.

The dashboard acts on these five. It ignores any other message it does not recognise, so a page can send extras without breaking.

navigated and open values are at most 512 characters, with no backslash or control character. A value that starts with // and any scheme other than https is refused.

From the dashboard to your page

typeFieldsWhen it arrives
tokentoken: a session tokenAfter every ready, and again before the previous token expires. Verify it on your server with the SDK's session verifier.
thememode: light or darkStraight after token, and whenever the merchant changes the dashboard theme.
resize-acknoneAfter each resize.
navigatepath: a path that starts with /When the merchant moves inside your app from the dashboard sidebar, or uses back and forward. Navigate in place; do not reload.

Exact origins

Both sides check where a message comes from, and neither sends to *.

  • The dashboard accepts a message only when its origin is exactly the origin of your page (the origin of merchant_page_url) and it came from the frame's own window. It posts to that exact origin.
  • Your page must do the same: accept messages only from the dashboard origin, https://dashboard.usequeek.com, and only from window.parent; and post to that origin. The SDK takes the dashboard origin as dashboardOrigin and applies both checks for you.

A message posted with the wrong target origin is never delivered, and nothing is logged in your page. This is the most common reason for a frame that loads but never becomes ready.

The handshake

The dashboard starts a clock when the frame mounts. It stops at the first ready that passes the origin and source checks.

Time with no valid readyWhat the merchant sees
5 secondsThe loading skeleton stays, with the line "Still loading… App is taking longer than usual."
15 secondsAn error card, "App didn't respond", with a Retry button.

The card says why, as far as the browser lets the dashboard know:

Reason shownMeaning
The address did not finish loading within 15 secondsThe frame's document never loaded: the app is down, starting up or very slow.
It loaded, but the app did not signal it was ready within 15 secondsThe document loaded, but no valid ready arrived. Typical causes: the page never posts ready, posts it to the wrong origin, shows an error page, or forbids framing.
Your browser could not open the addressA DNS, TLS or connection failure.
It was ready, but Queek could not start a session for itready arrived, and the dashboard could not mint the session token. Retry; contact support if it persists.

The 15 seconds count from the frame mounting, not from the page loading, so a slow first load through a tunnel counts against them. Only a valid ready stops the clock: resize and navigated messages do not. A page that posts resize and navigated but never ready still ends in the error card.

When the app does not load

  1. Open the page's address directly in a browser tab. If it fails there, fix that first.

  2. In the dashboard tab, open the browser console and watch the traffic:

    addEventListener('message', (event) => console.log(event.origin, event.data));

    A working page shows { source: "queek-app", type: "ready" } from your page's origin within a few seconds of the frame loading. If you see resize or navigated but no ready, the page does not announce itself.

  3. Check the response headers for X-Frame-Options and for a frame-ancestors rule that excludes https://dashboard.usequeek.com.

  4. Check that the origin your page posts to is the dashboard's, and that your SDK version is 0.7.2 or later.

How the frame is sized

The frame has no fixed height. It is as tall as the height your page reports in resize, between 120 and 1,600 pixels, and at least that tall. Until the first report it is 420 pixels.

This changes how layout units behave inside it:

  • The frame's viewport is the frame itself. 100vh and 100dvh resolve against the frame, whose height your page controls, so a layout built on them depends on its own size.
  • position: sticky and position: fixed are relative to the frame's own viewport. Because the frame is as tall as the page, nothing inside it scrolls relative to the frame, so a sticky or fixed bar does not stay in view while the merchant scrolls the dashboard.

Design for it instead of against it:

  • Let the page's height follow its content, and report it (the SDK does this).
  • For an editor with its own toolbars, build a panel of a fixed height inside the page: give the root element a height you choose (for example 720 pixels), let its lists and editors scroll inside it, and keep the toolbar in the panel. The page's height is then the panel's height, and the toolbar stays visible because the panel is shorter than the dashboard window.
  • Do not rely on the dashboard's scroll position or viewport. The contract has no message that shares either with the page.

Dialogs and overlays

Do not centre a modal in the frame. The frame is as tall as the page, so the centre of a long page can sit far below the merchant's window, and its buttons can end up under floating controls the dashboard shows at the bottom of the screen, with nothing to scroll them into view. A frame viewed in a test browser with a normal viewport does not show this; it only appears in the real dashboard.

  • Use inline confirmations: put the question and its buttons in a panel next to the thing they act on (under the row being removed, or in a bar at the top of the editor).
  • Keep menus and popovers anchored to their trigger and inside the visible part of the panel.
  • Never use alert, confirm or prompt. The sandbox ignores them.
  • To go somewhere in the dashboard, or to an external page, send open.
  • Test in the real dashboard through the Preview URL, at the window sizes merchants use, not only in a standalone browser tab or a headless browser.

Report where the merchant is, so a reload or a shared link returns to the same place.

  • Send navigated with the path and the query string, for example /admin/translate?locale=fr&type=product. A path without its query reloads to the page root without the filters, tabs or selection the merchant had.
  • The path must be under the page path of merchant_page_url. A path outside it is replaced by the page itself when the frame is built again.
  • Handle navigate by changing the page's route in place. The dashboard sends it when the merchant picks one of your sidebar entries or uses back and forward, and it does not reload the frame.

The dashboard mirrors your location in its own address as /apps?app=<slug>&path=<path>.

The app in the dashboard

  • A slim title bar above the frame shows the app's logo and name and a ⋯ menu: Manage app, pin or unpin, Get support (when the app has a support URL) and Uninstall. The page does not control the bar.
  • The sidebar's Apps group lists the apps the merchant pinned, plus the app that is open. The open app shows its own menu under it, from [[extensions.nav]] in the manifest. Each entry is a label and a path under the page path.
  • Moving between your pages through that menu sends navigate to the live page. It does not reload the frame.

SDK primitives

NeedUse
Announce ready and follow the dashboard themeinstallThemeListener({ dashboardOrigin }) from @usequeek/app-sdk/browser. From 0.7.2 it announces ready to the framing window by default. Versions up to 0.7.1 did not, so a page built only on it never became ready.
React<QueekProvider dashboardOrigin="https://dashboard.usequeek.com"> from @usequeek/app-sdk/react owns the bridge subscription and announces ready. useQueek() gives navigate.report(path) and navigate.open(target), navigate.onNavigate(handler), and the live theme.
First-load sessioninstallAuthFetch({ exchange, dashboardOrigin }) reads queek_token, removes it from the address, exchanges it once through your endpoint, and refreshes through ready and token.
Without a helpersendReady, sendResize, sendNavigated and sendOpen post one message each, to the exact origin you pass.

Keep the SDK at 0.7.2 or later. Pin an exact version: the SDK is pre-1.0.

Next

On this page