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-originYour page keeps its own origin, so scripts, forms and storage work. The sandbox grants no top-level navigation, no popups and no modal dialogs.
| Rule | Detail |
|---|---|
| Origin | The 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. |
| Framing | Your 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 load | The 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 dialogs | window.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
type | Fields | What the dashboard does |
|---|---|---|
ready | none | Ends the handshake and answers with token, then theme. Send it on every load, including after a reload inside the frame. |
resize | height: number of pixels | Sets the frame height (see How the frame is sized) and answers resize-ack. |
navigated | path: a path that starts with / | Mirrors your in-app location in the dashboard address, so reload, back and forward return to the same place. |
open | target: a dashboard path or an https URL | Opens a dashboard path in place, or an https URL in a new tab. |
ack | none | Accepted 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
type | Fields | When it arrives |
|---|---|---|
token | token: a session token | After every ready, and again before the previous token expires. Verify it on your server with the SDK's session verifier. |
theme | mode: light or dark | Straight after token, and whenever the merchant changes the dashboard theme. |
resize-ack | none | After each resize. |
navigate | path: 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 fromwindow.parent; and post to that origin. The SDK takes the dashboard origin asdashboardOriginand 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 ready | What the merchant sees |
|---|---|
| 5 seconds | The loading skeleton stays, with the line "Still loading… App is taking longer than usual." |
| 15 seconds | An error card, "App didn't respond", with a Retry button. |
The card says why, as far as the browser lets the dashboard know:
| Reason shown | Meaning |
|---|---|
| The address did not finish loading within 15 seconds | The 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 seconds | The 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 address | A DNS, TLS or connection failure. |
| It was ready, but Queek could not start a session for it | ready 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
-
Open the page's address directly in a browser tab. If it fails there, fix that first.
-
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 seeresizeornavigatedbut noready, the page does not announce itself. -
Check the response headers for
X-Frame-Optionsand for aframe-ancestorsrule that excludeshttps://dashboard.usequeek.com. -
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.
100vhand100dvhresolve against the frame, whose height your page controls, so a layout built on them depends on its own size. position: stickyandposition: fixedare 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,confirmorprompt. 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.
Navigation and reload
Report where the merchant is, so a reload or a shared link returns to the same place.
- Send
navigatedwith 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
navigateby 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
navigateto the live page. It does not reload the frame.
SDK primitives
| Need | Use |
|---|---|
Announce ready and follow the dashboard theme | installThemeListener({ 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 session | installAuthFetch({ exchange, dashboardOrigin }) reads queek_token, removes it from the address, exchanges it once through your endpoint, and refreshes through ready and token. |
| Without a helper | sendReady, 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
App SDK
Use @usequeek/app-sdk for install handoffs, embedded session tokens, webhook verification and a typed Merchant API client, with a working server example.
Translate store content
Build a translation app on the Merchant API. The resource types and their ids, how to read source text, write translations safely, count progress per language, and the errors you will meet.