Skip to main content
A Salesive app can ship an embedded UI that opens inside the merchant’s dashboard — like a panel or full-screen sheet — alongside its backend API access. This page documents how that works: the iframe environment, the background pre-load system, lifecycle events, and the runtime permissions the merchant can grant at any time.

How the embedded UI works

When a merchant opens your app from the dashboard dock, your app’s appUrl is loaded in a sandboxed <iframe> inside the dashboard. The dashboard appends a few context parameters so your app can recognise the active store and user without a separate auth step:
These parameters are for convenience. They identify the context but do not prove identity. To prove which store and user is calling your backend, send a session token from getSessionToken() and verify it on your server. A signed session cookie set during the OAuth install also works. Never trust shop/user from the URL on their own.
Apps can also render inside the order, catalog item and customer pages, as an action that opens in a modal or a block card. Those frames are separate from the full-screen app described here and have their own small set of messages. See Dashboard UI extensions.

Background pre-loading

The dashboard pre-loads every installed app in a hidden iframe when the merchant opens the dashboard — before the merchant ever taps the dock icon. This means:
  • Your app’s code, assets, and connections are already warm by the time the merchant opens it.
  • Background work (polling, WebSocket connections, sync) can start immediately, not only when the merchant looks at the app.
  • The first open is instant — no loading spinner if your app has initialised in the background.
When your app’s iframe loads in the background, the dashboard sends a salesive:app-wake postMessage. Register a handler for this to start background work:
The dashboard pre-loads background iframes with real dimensions (not 0×0) and off-screen positioning so browsers do not throttle their JavaScript execution. Your timers, WebSocket connections, and fetch() calls all work normally in the background.

Full lifecycle

Every embedded app goes through these events:
The app keeps running through every state change. Only an uninstall removes the iframe.

Session boundaries

Two further events fire when the ground shifts underneath your app — the merchant is no longer the same person, or is no longer looking at the same store:
Both are broadcast to every installed app, foreground or background — a backgrounded app is holding the departing merchant’s data just as wrongly as the visible one.
Handlers for these two events must be synchronous and fast. Both announce that your iframe is about to be torn down, and the dashboard only pauses briefly (~150 ms on logout) before doing it. An await on a network round trip will not finish. Clear local state synchronously; if your server must also be told, use navigator.sendBeacon(), which is designed to survive teardown.

Runtime permissions

Some capabilities require explicit, time-limited merchant approval: expanding the app programmatically, reading the clipboard, cancelling orders, managing domains and DNS. Unlike OAuth install scopes (which are approved once at install), runtime permissions can be requested at any time via a dashboard modal the merchant approves or denies. The merchant chooses a duration (once, 1 day, 7 days, etc.) and can revoke at any time.

Available runtime permissions

Runtime permissions are separate from OAuth scopes. OAuth scopes gate the backend API. Runtime permissions gate capabilities the merchant approves after install, including API-backed writes such as WRITE_DOMAINS and WRITE_DNS. Your app needs both: the right OAuth scope to call an API endpoint, and the right runtime permission for the actions that require one.
WRITE_DOMAINS does not reach the full DNS zone replace. PUT /domains/:id/dns is refused for app tokens; apps change DNS one record at a time with WRITE_DNS.

File picker

Your app can open the merchant’s Salesive media library and receive the files they pick — without building any upload UI or file storage of your own. Call pickFiles() and the dashboard shows the same media manager used across Salesive: the merchant selects existing files (or uploads new ones inline), and your app receives their public URLs.
The file picker needs no runtime permission — the merchant explicitly chooses each file in the modal, the same consent model as a native file input. The picker only opens while your app is in the foreground; a call from a backgrounded app resolves to an empty array. See the pickFiles() reference below for the full API.

The App Bridge package

Install the helper:
Import from the permissions subpath for the smallest bundle (tree-shakes the CLI and Vite plugin out):
Or from the main package (same exports):
TypeScript types are bundled — no @types/ package needed.

onAppWake(callback)

Fires when the dashboard first loads the app in the background. This is the earliest point at which your app’s JavaScript runs. Use it to start long-running background work.

onAppOpened(callback)

Fires each time the merchant expands the app to full view (first open or restore from minimized). Use it to refresh UI data that may be stale after time in the background.

onAppMinimized(callback)

Fires when the merchant presses − (minimize) or swipes the sheet down on mobile. The app is hidden but still running.

onAppClosed(callback)

Fires when the merchant presses × (close / dismiss). The app is still running in the background — this is the same as minimized from a lifecycle standpoint, but signals that the merchant deliberately dismissed the panel rather than just hiding it.

onLogout(callback)

Fires when the merchant logs out of Salesive. The dashboard clears only its own storage on logout. Your app runs on a different origin, so its cookies, localStorage, and tokens survive untouched — the next person to log in on that browser would silently inherit the previous merchant’s app session. This event is your only opportunity to prevent that.
Keep this handler synchronous. The dashboard sends the event, waits ~150 ms, then redirects to the login page — which destroys your iframe and anything still queued in it. await fetch("/api/session", { method: "DELETE" }) will not complete; use navigator.sendBeacon("/api/session") instead.
Your app also receives the merchant’s user id as a query param on every embed. Checking it on boot is a useful belt-and-braces companion to this event: if user differs from the one your stored session belongs to, force re-authentication regardless of whether the logout event was delivered.

onStoreChanged(callback)

Fires when the merchant switches the active store. A merchant can own several stores and swap between them without reloading the dashboard, so anything your app cached — orders, products, settings — belongs to the store they just left.
Your iframe reloads immediately after this event, with the new store’s id in the shop query param. That reload — not this callback — is what boots your app under the new store, so keep reading shop from the URL on startup exactly as you already do. Treat this callback as the “the rug is about to be pulled” hook: use it to drop caches that outlive a reload (IndexedDB, Cache Storage, localStorage) and to save in-progress work. An app that misses the event still starts up correctly against the right store; it just loses the chance to flush stale data first.

requestPermission(permission, options?, timeoutMs?)

Show the merchant a permission modal. Returns true if approved, false if denied or timed out. The modal has a 10-second auto-deny countdown.
options can stay a plain duration array, so existing calls keep working. Pass an options object when the permission needs a scope (WRITE_DNS) or a reason the merchant should see.
If you do not use the helper package, send the same fields in the raw salesive:request-permission postMessage. See DNS records. Duration options

getGrantedPermissions(timeoutMs?)

Silently retrieve the merchant’s active grants — no modal shown. Call this on wake or mount to avoid requesting a permission the merchant already approved.
Returns a map of Permission → GrantRecord:

onLaunch()

Ask the dashboard to expand (un-minimize) this app. The call is silently ignored if AUTO_LAUNCH has not been granted.

pickFiles(options?)

Open the merchant’s Salesive media library and resolve with the files they select. Always resolves with an array of files (even in single-select mode), or an empty array if the merchant closes the picker without choosing. The picker only opens while your app is in the foreground.
size is a human-readable string ("1.2 MB"), not a raw byte count. The returned url is a hosted, publicly reachable link to the merchant’s existing media — store it or use it directly.

getSessionToken(options?)

Get a short lived (60 second) JWT, signed by Salesive, that proves to your own backend which store (dest) and which signed in user (sub) is using your app. Send it as Authorization: Bearer <token> and verify it on your server with the Salesive public key. Your backend can also trade it for an access token (token exchange). The full guide, with claims and verification code, is on Session tokens.
The token is a 60 second JWT. The dashboard caches one per installation and refreshes it only when under 10 seconds remain, so an app that calls getSessionToken() before every request presents the same token repeatedly for about 50 seconds. Call it fresh each time rather than storing the token yourself.
Session token verification must be idempotent. Do not treat a token as single use, do not store its jti to reject replays, and do not expire a token on first use. The dashboard hands the same token to every getSessionToken() call until it refreshes, so a verifier that rejects reuse will break the app on its second request.
It works in the background too (for example inside onAppWake). It rejects with a SessionTokenError whose code is not_embedded, timeout, not_installed (a developer preview on a store without the app), or the dashboard’s error. The cache is dropped on logout and store switch. Wire protocol: the app posts { type: "salesive:get-session-token", requestId } and the dashboard answers once with { type: "salesive:session-token", requestId, token, expiresAt } or { type: "salesive:session-token", requestId, error }.

onEvent(type, callback)

Subscribe to any postMessage event by type string — including future event types not yet in this helper. Callbacks receive the full raw message data object.
APP_EVENTS constants

Extension functions

isExtension(), getExtensionContext(timeoutMs?), onExtensionContext(callback), resizeExtension(height), autoResizeExtension() and closeExtension({ reload }) are for pages that run as dashboard UI extensions (actions and blocks on the order, catalog item and customer pages). The full reference, with message names and limits, is on that page.

Complete React example

The same app as a block on the order page

One build can serve both the full-screen app and its extensions. Branch on isExtension() at the root:

Environment notes

  • Browser only — all functions are no-ops in Node.js / SSR contexts (return false, {}, or a no-op unsubscribe). Safe to import in Next.js or Remix server components.
  • No runtime dependencies — the permissions subpath has zero dependencies.
  • Single listener — one window.addEventListener("message", …) is registered on module import. Multiple concurrent requestPermission() calls resolve independently.

Next steps

Session tokens

Prove the store and user to your backend, and install without a redirect.

Dashboard UI extensions

Actions and blocks on the order, catalog item and customer pages.

AI tools (WebMCP)

Register tools that Ola can call, with merchant approval.

OAuth install flow

Set up the token exchange and session binding.

Scopes & permissions

The API scopes your backend calls need.

Build & publish

Submit your app for marketplace review.

Webhooks

Real-time store events for your backend.