Skip to content
Saleor
Esc
↑↓navigate↵open⌘Jpreview
On this page

AppBridge

AppBridge is an interface that connects an App (running inside Dashboard) with the Dashboard itself.

Setup

Create instance of AppBridge by running the following code:

import { AppBridge } from "@saleor/app-sdk/app-bridge";

const appBridge = new AppBridge(options);

Options object is the following:

type AppBridgeOptions = {
  saleorApiUrl?: string;
  initialLocale?: LocaleCode;
  autoNotifyReady?: boolean;
  initialTheme?: "dark" | "light";
  /**
   * Forward keyboard shortcuts owned by the Dashboard (e.g. Cmd+K) out of the app iframe.
   * Defaults to true. See "Keyboard shortcuts" below.
   */
  forwardKeyboardShortcuts?: boolean | { shouldForward(event: KeyboardEvent): boolean };
};

AppState

You can get the current state of the app by calling appBridge.getState():

const { token, saleorApiUrl, ready, id } = appBridge.getState();

Available state represents AppBridgeState:

type AppBridgeState = {
  /**
  * JWT token provided by Dashboard. Represents user's session.
  */
  token?: string;
  /**
  * ID of the app
  */
  id: string;
  /**
  *  Flag if app bridge has properly initialized and authorized
  */
  ready: boolean;
  /**
  * Current path on the frontend
  */
  path: string;
  theme: ThemeType;
  locale: LocaleCode; // See src/locales.ts
  /**
   * Full URL including protocol and path where GraphQL API is available
   **/
  saleorApiUrl: string;
  /**
   * Versions of Saleor that app is being installed.
   */
  saleorVersion?: string;
  dashboardVersion?: string;
  user?: {
    /**
     * Original permissions of the user that is using the app.
     * *Not* the same permissions as the app itself.
     *
     * Can be used by app to check if user is authorized to perform
     * domain specific actions
     */
    permissions: Permission[];
    email: string;
  };
  /**
   * Permissions of the app itself
   */
  appPermissions?: AppPermission[];
  /**
   * Arbitrary payload passed by the `openPopup` action when this app was opened
   * as a POPUP from one of its own widgets. See "Opening a popup extension".
   */
  appParams?: unknown;
  /**
   * Internal - used by Dashboard and apps to negotiate inner keyboard shortcuts propagation.
   */
  dashboardShortcuts?: DashboardShortcut[];
};

AppBridgeProvider

AppBridgeProvider and useAppBridge hook are exposed from @saleor/app-sdk:

// app.tsx
import { AppBridgeProvider } from "@saleor/app-sdk/app-bridge";

<AppBridgeProvider>
  <YourApp />
</AppBridgeProvider>;

AppBridgeProvider can optionally receive AppBridge instance in props, otherwise it will create one automatically.

useAppBridge hook

In components wrapped with AppBridgeProvider, you can use the useAppBridge hook:

import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useEffect } from "react";

const MyComponent = () => {
  const { appBridge, appBridgeState } = useAppBridge();

  useEffect(() => {
    appBridge?.dispatch(/* Something */);
  }, [appBridge]);

  return <div>Current locale is: {appBridgeState?.locale}</div>;
};

appBridgeState? and appBridge can be nullish, because they don’t exist in the server side context.

Events

Events are messages that originate in Saleor Dashboard. AppBridge can subscribe to events and the App can react to them.

Subscribing to events

subscribe(eventType, callback) - can be used to listen to particular event type. It returns an unsubscribe function, which unregister the callback.

Example:

const unsubscribe = appBridge.subscribe("handshake", (payload) => {
  setToken(payload.token); // do something with event payload

  const { token } = appState.getState(); // you can also get app's current state here
});

// unsubscribe when callback is no longer needed
unsubscribe();

Unsubscribing multiple listeners

unsubscribeAll(eventType?) - unregister all callbacks of provided type. If no type was provided, it will remove all event callbacks.

Example:

// unsubscribe from all handshake events
appBridge.unsubscribeAll("handshake");

// unsubscribe from all events
appBridge.unsubscribeAll();

Available event types

Event type Description
handshake Fired when iFrame containing the App is initialized or new token is assigned
response Fired when Dashboard responds to an Action
redirect Fired when Dashboard changes a subpath within the app path
theme Fired when Dashboard changes the theme
localeChanged Fired when Dashboard changes locale (and passes locale code in payload)
tokenRefresh Fired when Dashboard receives a new auth token and passes it to the app
shortcutsChanged Internal. Fired when Dashboard changes the set of keyboard shortcuts it owns. Handled automatically by AppBridge

See source code for detailed payload

Actions

Actions expose a high-level API to communicate with Saleor Dashboard. They’re exported under an actions namespace.

Available methods

dispatch(action) - dispatches an Action. Returns a promise which resolves when action is successfully completed.

Example:

import { actions } from "@saleor/app-sdk/app-bridge";

const handleRedirect = async () => {
  await appBridge.dispatch(actions.Redirect({ to: "/orders" }));

  console.log("Redirect complete!");
};

handleRedirect();

Available actions

Action Arguments Description
Redirect to (string) - relative (inside Dashboard) or absolute URL path
newContext (boolean) - should open in a new browsing context
Notification status (info / success / warning / error / undefined)
title (string / undefined) - title of the notification
text (string / undefined) - content of the notification
apiMessage (string / undefined) - error log from api
NotifyReady Inform Dashboard that AppBridge is ready
UpdateRouting newRoute - current path of App to be set in URL
RequestPermissions permissions - array of Permissions you want to add, redirectPath - value of query param app will receive in ?redirectUrl= after user approves/rejects permissions Ask Dashboard to give more permissions to the app. Dashboard will unmount app. After user approves or denies, Dashboard will redirect to redirectPath. If operation fails, ?error=REASON will be appended
PopupClose (>=3.22.31) Ask Dashboard to close the popup. If the app is running in a popup, Dashboard will close it. If the app is not in a popup, this action does nothing and responds with ok: true.
WidgetResize height - widget content height in pixels (positive, finite) Ask Dashboard to resize the widget iframe to match the app’s content height. Only affects *_DETAILS_WIDGETS extensions. See Sizing sidebar widgets for the recommended helpers.
RefreshEntity (>=3.23.9) Ask Dashboard to refresh the entity active in the current context (e.g. the currently open Order or Product), without a full page reload. Requires @saleor/app-sdk 1.11 or newer and Saleor Dashboard 3.23.9 or newer.
OpenPopup (>=3.23.19) extensionIdentifier (string) - app-defined identifier of the target POPUP extension to open. params (optional, JSON-serializable) - arbitrary payload forwarded to the opened popup Ask Dashboard to open one of the same app’s POPUP extensions in full (modal) mode. Intended to be dispatched from a WIDGET extension to open a co-located POPUP. Requires @saleor/app-sdk 1.12 or newer and Saleor 3.23.19 or newer. See Opening a popup extension.
RedirectToApp appIdentifier (string) - manifest id of the target app. path (optional string) - path inside the target app Ask Dashboard to resolve the URL of another installed app and redirect there. Requires @saleor/app-sdk 1.15 or newer. See Redirecting to another app.

Redirecting to another app

Apps often work together — a checkout app may want to hand the user off to the app that owns tax configuration. Instead of hardcoding the other app’s URL (which depends on where it is installed), dispatch actions.RedirectToApp() with the target app’s manifest id. The Dashboard resolves that app’s URL and navigates to it.

import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";

const { appBridge } = useAppBridge();

const openTaxesApp = async () => {
  await appBridge.dispatch(
    actions.RedirectToApp({
      appIdentifier: "saleor.app.avatax",
      // Optional: path inside the target app, appended to the resolved URL
      path: "/configuration",
    }),
  );
};

appIdentifier is required and must be a non-empty string — the SDK throws synchronously otherwise. It is the id field from the target app’s manifest, not its Saleor object ID.

Refreshing the active entity

When an app mutates data that the Dashboard is currently displaying — for example, an order widget that adds a note or changes fulfillment — the Dashboard’s view can become stale until the user manually reloads. Dispatch actions.RefreshEntity() to ask the Dashboard to re-fetch the entity active in the current context.

import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";

const { appBridge } = useAppBridge();

const handleSaved = async () => {
  // ...perform the mutation that changes the open Order/Product...

  await appBridge.dispatch(actions.RefreshEntity());
};

The action takes no arguments: the Dashboard already knows which entity is open in the current context (the mount where your app is rendered, such as ORDER_DETAILS_WIDGETS or PRODUCT_DETAILS_WIDGETS).

Opening a popup extension

A sidebar WIDGET is small by design, so complex flows don’t fit well inside it. Instead of cramming everything into the widget, you can dispatch actions.OpenPopup() to ask the Dashboard to open one of your app’s POPUP extensions in full (modal) mode — a “compact widget that expands into a full view” pattern.

The Dashboard resolves the target popup by its identifier and renders it in a modal dialog as a regular app iframe.

Declaring the popup extension

The target POPUP must belong to the same app and declare a stable identifier in the manifest. The identifier is what you pass to the action as extensionIdentifier:

{
  "extensions": [
    {
      "label": "Order tools",
      "mount": "ORDER_DETAILS_WIDGETS",
      "target": "WIDGET",
      "permissions": ["MANAGE_ORDERS"],
      "url": "/widgets/order"
    },
    {
      "label": "Order tools (full view)",
      "identifier": "order-tools-popup",
      "mount": "ORDER_DETAILS_MORE_ACTIONS",
      "target": "POPUP",
      "permissions": ["MANAGE_ORDERS"],
      "url": "/popups/order-tools"
    }
  ]
}

The identifier must be unique within a single app (an app cannot reuse the same identifier for two of its extensions), but the same value may be used by different apps. See extension identifier.

Dispatching the action

Dispatch actions.OpenPopup() from the widget, referencing the popup’s identifier:

import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";

const { appBridge } = useAppBridge();

const handleExpand = async () => {
  await appBridge.dispatch(
    actions.OpenPopup({
      extensionIdentifier: "order-tools-popup",
      // Optional: forward arbitrary, JSON-serializable data to the popup
      params: { mode: "full", focusLineId: "T3JkZXJMaW5lOjE=" },
    }),
  );
};

extensionIdentifier is required and must be a non-empty string — the SDK throws synchronously otherwise. params is optional.

Passing data to the popup

The optional params payload is forwarded to the opened popup. The Dashboard serializes it into the popup iframe URL, and the SDK decodes it on load and exposes it on the App Bridge state as appParams. Read it via useAppBridge() — no URL parsing required:

import { useAppBridge } from "@saleor/app-sdk/app-bridge";

const OrderToolsPopup = () => {
  const { appBridgeState } = useAppBridge();
  const params = appBridgeState?.appParams; // { mode: "full", focusLineId: "..." } | undefined

  // ...render based on params
};

appParams is undefined when the app wasn’t opened via OpenPopup. Because params travels in the URL, it must be JSON-serializable and is size-limited (the serialized JSON is capped at ~2 KB). Use it for small references (IDs, a mode flag), not for bulk data — fetch larger data from your backend using those references.

Constraints

  • Widget-only. The action is only honored when dispatched from a WIDGET extension. Requests from other targets are rejected.
  • Same app. A widget can only open a POPUP that belongs to the same app — you cannot open another app’s extension.
  • Co-located. The target popup must be one of the app’s extensions registered on the current page. If no matching POPUP with the given identifier is found, the action fails (resolves with ok: false).

Sizing sidebar widgets

Detail-page widgets (PRODUCT_DETAILS_WIDGETS, ORDER_DETAILS_WIDGETS, and other *_DETAILS_WIDGETS mounts) render in the entity sidebar. By default, each widget iframe keeps a fixed height, which can clip content or leave empty space when your UI is shorter or taller than that box.

When you report your widget’s content height, the Dashboard resizes the iframe to match. Staff see the full widget without scrolling inside a tiny frame, and multiple app widgets stack naturally in the sidebar without internal scrollbars.

Reporting height is opt-in. Apps that do not use the helpers below keep the previous fixed height.

React apps

Wrap your widget UI in an element with a ref, then call useWidgetAutoResize inside AppBridgeProvider (see AppBridgeProvider). The hook uses useAppBridge() internally, measures the root element, and dispatches WidgetResize when the layout changes (after App Bridge becomes ready).

import { useRef } from "react";
import { useWidgetAutoResize } from "@saleor/app-sdk/app-bridge";

export default function ProductWidget() {
  const rootRef = useRef<HTMLDivElement>(null);

  useWidgetAutoResize(rootRef);

  return (
    <div ref={rootRef}>
      {/* Your widget content */}
    </div>
  );
}

Use this on routes served for target: "WIDGET" extensions on *_DETAILS_WIDGETS mounts. See Extending Dashboard with Apps for manifest setup.

Without the hook

If you are not using React, or you need to report height after a one-off layout change (for example, when async data finishes loading), pass an AppBridge instance to the lower-level helpers (from useAppBridge() or your own new AppBridge()):

Helper Use when
reportWidgetHeightFromElement(appBridge, root) You have a root element; measure it and dispatch WidgetResize in one call
reportWidgetHeight(appBridge, height) You already know the height in pixels

Helpers no-op during SSR. reportWidgetHeightFromElement requires a widget root element.

Protocol

Height is reported through the standard App Bridge channel — reportWidgetHeight and reportWidgetHeightFromElement call appBridge.dispatch(actions.WidgetResize({ height })), the same mechanism as redirect, notification, and other actions. If you prefer, you can dispatch it yourself:

import { actions } from "@saleor/app-sdk/app-bridge";

appBridge.dispatch(actions.WidgetResize({ height: 320 })).catch(() => {
  // Best-effort: sizing helpers swallow rejections (timeout or negative response).
});

The helpers are the recommended path: they no-op during SSR, skip invalid heights (non-finite or non-positive), and attach a .catch so a missing or slow Dashboard response does not surface as an unhandled rejection.

Until the first report, the iframe uses a 200px default height. Reported heights are capped at 5000px.

Keyboard shortcuts

Dashboard-level shortcuts (such as Cmd+K) would otherwise stop working while the app iframe has focus. The Dashboard and AppBridge negotiate this internally with the shortcutsChanged event and the triggerShortcut action — no code is required in your app, and there is no actions.TriggerShortcut() helper.

Pass forwardKeyboardShortcuts: false to new AppBridge() to opt out, or { shouldForward(event) } to claim a chord back for the app.

Detaching AppBridge

appBridge.destroy() detaches the instance from the window: it stops forwarding keyboard shortcuts, stops receiving Dashboard events, and drops all subscribers. It is safe to call more than once.

You rarely need to call it directly — AppBridgeProvider destroys the instance it created when it unmounts. An instance passed via the appBridgeInstance prop is left alone, because the caller owns its lifecycle.

const appBridge = new AppBridge();

// later, e.g. when tearing down a non-React host
appBridge.destroy();

Last updated on August 24, 2026

Was this page helpful?