> ## Documentation Index
> Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt
> Use this file to discover all available pages before exploring further.

# App Bridge Overview

> Communicate between your app iframe and the LaunchMyStore host

# App Bridge

App Bridge is the postMessage-based SDK that brokers communication between
your app's iframe and the LaunchMyStore host page — whether that host is the
admin (the admin), the checkout, or the post-purchase / order-status
page. It speaks one wire protocol everywhere; what changes is which
actions the host happens to wire up.

The SDK package is **`@launchmystore/app-bridge`** (vanilla TS) with a thin
React adapter at **`@launchmystore/app-bridge-react`**.

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm install @launchmystore/app-bridge
  ```

  ```bash yarn theme={null}
  yarn add @launchmystore/app-bridge
  ```

  ```bash pnpm theme={null}
  pnpm add @launchmystore/app-bridge
  ```
</CodeGroup>

<Note>
  The SDK ships as **CJS + ESM** only (`dist/index.js` and `dist/index.mjs`).
  There is no public CDN build — if you need an inline build, build the
  SDK locally and serve the bundle yourself.
</Note>

## Quick Start

```javascript theme={null}
import { createApp } from '@launchmystore/app-bridge';

const app = createApp({
  apiKey: 'your-app-client-id',
  host: new URLSearchParams(location.search).get('host'), // base64-encoded origin
});

// Fire-and-forget: show a toast on the host
app.dispatch('TOAST_SHOW', {
  message: 'Saved!',
  duration: 3000,
  type: 'success',
});

// Round-trip: open a resource picker and wait for the selection
const result = await app.dispatchAndWait('RESOURCE_PICKER_OPEN', {
  resourceType: 'Product',
  multiple: true,
});
console.log(result.selection);
```

<Warning>
  `app.dispatch(action, payload)` and `app.dispatchAndWait(action, payload)`
  take the action name as the **first argument**. Do not pass a
  `{ type, payload }` envelope — the host filters on `type:
    'APP_BRIDGE_ACTION'` plus the `action` field, and a `{type, payload}` call
  would post a malformed message that the host silently drops.
</Warning>

## Core API

The `App` instance returned by `createApp()` exposes four methods:

| Method                              | Returns                    | Use for                                                                                               |
| ----------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------- |
| `dispatch(action, payload?)`        | `string` (message id)      | Fire-and-forget actions like `TOAST_SHOW`, `REDIRECT`, `MODAL_CLOSE`.                                 |
| `dispatchAndWait(action, payload?)` | `Promise<payload>`         | Round-trip actions: `SESSION_TOKEN_REQUEST`, `CART_GET`, `CLIPBOARD_READ_REQUEST`. Rejects after 10s. |
| `subscribe(action, cb)`             | `() => void` (unsubscribe) | Listen for host-initiated actions like `RESOURCE_PICKER_SELECTION`, `MODAL_PRIMARY_ACTION`.           |
| `getSessionToken()`                 | `Promise<string>`          | Shortcut for `dispatchAndWait('SESSION_TOKEN_REQUEST')` with caching.                                 |

### dispatch()

```javascript theme={null}
app.dispatch('TOAST_SHOW', { message: 'Hello!', type: 'info' });
app.dispatch('MODAL_CLOSE');                  // no payload
app.dispatch('REDIRECT', { url: '/orders' });
```

### dispatchAndWait()

```javascript theme={null}
const { token } = await app.dispatchAndWait('SESSION_TOKEN_REQUEST');

const cart = await app.dispatchAndWait('CART_GET');
console.log(cart.items, cart.itemCount, cart.currency);
```

Promise behaviour:

* **Resolves** with the host's response payload (the bare payload — not
  wrapped in another `payload` key).
* **Rejects** after **10 seconds** with
  `Error('App Bridge: timeout waiting for <action> response')`.
* **Rejects** if the host returns an `error` string — the SDK rejects with
  `new Error(error)`, i.e. the host's error message **verbatim** (no
  `App Bridge: <action> failed:` prefix).

```javascript theme={null}
try {
  const result = await app.dispatchAndWait('RESOURCE_PICKER_OPEN', {
    resourceType: 'Product',
  });
} catch (err) {
  // No `err.code` — branch on err.message
  if (err.message.includes('timeout')) {
    console.error('Host did not respond in 10s');
  } else {
    console.error(err.message);
  }
}
```

### subscribe()

```javascript theme={null}
const unsubscribe = app.subscribe('MODAL_PRIMARY_ACTION', (payload) => {
  console.log('User clicked primary button on modal', payload.modalId);
});

// Later
unsubscribe();
```

### getSessionToken()

```javascript theme={null}
const token = await app.getSessionToken();

const response = await fetch('/api/orders', {
  headers: { Authorization: `Bearer ${token}` },
});
```

Note that `app.getSessionToken()` is **not** cached — it issues a fresh
`SESSION_TOKEN_REQUEST` on every call. Caching (return the cached token
while more than 30s from expiry, otherwise refresh) lives in the
`SessionToken` action class and the `useSessionToken` React hook. Use those
if you want caching. See [Sessions & Authentication](/app-bridge/session-tokens).

## Action Surface

Action names are **screaming-snake-case verbs**, not object types. The SDK
exports helper classes for the actions below in
`@launchmystore/app-bridge/actions` — but whether an action does anything
on a given page depends on what that **host** has wired. The per-host
pages linked from the [Hosts](#hosts) table are the authoritative source
of truth.

### SDK-exposed action families

| Family                                   | Actions                                                                                                                                                                                                                                                                                                                                | Hosts that wire it                                              |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Toast                                    | `TOAST_SHOW`, `TOAST_DISMISS`                                                                                                                                                                                                                                                                                                          | Admin, Checkout, Post-purchase                                  |
| Modal                                    | `MODAL_OPEN`, `MODAL_CLOSE`, `MODAL_PRIMARY_ACTION`, `MODAL_SECONDARY_ACTION`                                                                                                                                                                                                                                                          | Admin                                                           |
| ResourcePicker                           | `RESOURCE_PICKER_OPEN`, `RESOURCE_PICKER_CLOSE`, `RESOURCE_PICKER_SELECTION`, `RESOURCE_PICKER_CANCEL`                                                                                                                                                                                                                                 | Admin                                                           |
| TitleBar                                 | `TITLE_BAR_UPDATE`, `TITLE_BAR_PRIMARY_ACTION`, `TITLE_BAR_SECONDARY_ACTION`                                                                                                                                                                                                                                                           | Admin                                                           |
| NavigationMenu                           | `NAV_MENU_UPDATE`, `NAV_MENU_CLICK`                                                                                                                                                                                                                                                                                                    | Admin                                                           |
| ContextualSaveBar                        | `CONTEXTUAL_SAVE_BAR` (payload `action: SHOW \| HIDE \| UPDATE`), host events `CONTEXTUAL_SAVE_BAR_SAVE`, `CONTEXTUAL_SAVE_BAR_DISCARD`                                                                                                                                                                                                | Admin                                                           |
| Loading                                  | `LOADING_START`, `LOADING_STOP`                                                                                                                                                                                                                                                                                                        | Admin                                                           |
| LeaveConfirmation                        | `LEAVE_CONFIRMATION_ENABLE`, `LEAVE_CONFIRMATION_DISABLE`                                                                                                                                                                                                                                                                              | Admin                                                           |
| SessionToken                             | `SESSION_TOKEN_REQUEST`                                                                                                                                                                                                                                                                                                                | Admin only                                                      |
| Clipboard                                | `CLIPBOARD_READ_REQUEST` (writes run iframe-side via `navigator.clipboard`)                                                                                                                                                                                                                                                            | Admin (`CLIPBOARD_WRITE` also handled on Admin + Post-purchase) |
| History                                  | `HISTORY_PUSH`, `HISTORY_REPLACE`, `HISTORY_GO`, `HISTORY_BACK`, `HISTORY_FORWARD`                                                                                                                                                                                                                                                     | Admin                                                           |
| Redirect                                 | `REDIRECT`                                                                                                                                                                                                                                                                                                                             | Admin, Post-purchase                                            |
| Lifecycle                                | host-pushed `APP_FOCUS`, `APP_BLUR`, `APP_VISIBLE`, `APP_HIDDEN`                                                                                                                                                                                                                                                                       | Admin                                                           |
| Bridge                                   | `BRIDGE_PING`                                                                                                                                                                                                                                                                                                                          | Checkout, Post-purchase                                         |
| **Cart / Buyer-journey** (checkout-only) | `CART_GET`, `CART_LINES_CHANGE`, `DISCOUNT_CODE_CHANGE`, `GIFT_CARD_CHANGE`, `NOTE_CHANGE`, `ATTRIBUTE_CHANGE`, `METAFIELD_CHANGE`, `CHECKOUT_TOTALS_GET`, `CUSTOMER_GET`, `CURRENCY_GET`, `ORDER_NOTE_SET`, `COUPON_APPLY_REQUEST`, `BUYER_JOURNEY_INTERCEPT_REQUEST` (host→iframe), `BUYER_JOURNEY_INTERCEPT_RESPONSE` (iframe→host) | Checkout                                                        |
| **Post-purchase** (subset)               | `CART_LINES_CHANGE` (add only), `ORDER_GET`, `CUSTOMER_GET`, `CURRENCY_GET`, `REDIRECT`, `DONE`                                                                                                                                                                                                                                        | Post-purchase                                                   |

<Note>
  The **admin host also handles** `USER_FETCH`, `CONFIG_FETCH`,
  `CONFIG_GET`, `ENVIRONMENT_FETCH`, `FEATURES_QUERY`, `PRINT`, `SHARE`,
  `SCANNER_OPEN`/`SCANNER_CLOSE`, `FULLSCREEN_ENTER`/`EXIT`/`TOGGLE`,
  `INTENT_LAUNCH`, `EXTENSION_CONTEXT_GET`, and `APP_BRIDGE_READY`. These
  work only in admin contexts — dispatching them from a checkout or
  post-purchase iframe times out after 10 seconds. Note the SDK's
  `Features` class is a local capability check and never round-trips;
  use raw `dispatchAndWait('FEATURES_QUERY')` if you want the host's own
  answer.
</Note>

Counts vary as the surface grows. See:

* [Actions Reference](/app-bridge/actions) — payload shapes per action.
* [App Bridge for Checkout](/app-bridge/checkout) — cart / discount / note / attribute mutators and the buyer-journey intercept.

## Wire Format

The protocol is small enough to implement without the SDK if you need to.
The iframe posts:

```js theme={null}
window.parent.postMessage({
  type: 'APP_BRIDGE_ACTION',
  action: 'TOAST_SHOW',           // screaming-snake action name
  id: 'ab_1700000000000_1',       // round-trip correlation id (any unique string)
  payload: {
    message: 'Saved',
    apiKey: 'your-app-client-id', // the SDK appends this for you
  },
}, '*');
```

The host replies:

```js theme={null}
{
  type: 'APP_BRIDGE_RESPONSE',
  action: 'TOAST_SHOW',
  id: 'ab_1700000000000_1',
  payload: { ok: true },
  // error?: string — present on failure
}
```

The iframe also self-resizes via a **separate** message type:

```js theme={null}
window.parent.postMessage({
  type: 'APP_BRIDGE_RESIZE',
  extensionId: '<your manifest handle>',  // required so the host knows which iframe to resize
  height: document.body.scrollHeight,
}, '*');
```

<Note>
  Resize clamps depend on the host. Admin blocks: default 200, max 2000.
  Admin Action modals: default 400, max 800 (and **no** `extensionId` is
  echoed — the modal already knows which extension it loaded). Checkout
  extension slots: default 60, max 2000.
</Note>

## React Integration

The React adapter wraps the same SDK in a context provider plus a hook for
every action family.

```bash theme={null}
npm install @launchmystore/app-bridge @launchmystore/app-bridge-react
```

```jsx theme={null}
import { AppBridgeProvider, useToast, useSessionToken } from '@launchmystore/app-bridge-react';

function Root() {
  return (
    <AppBridgeProvider
      config={{
        apiKey: process.env.NEXT_PUBLIC_APP_CLIENT_ID,
        host: new URLSearchParams(location.search).get('host'),
      }}
    >
      <SaveButton />
    </AppBridgeProvider>
  );
}

function SaveButton() {
  const toast = useToast();      // { show, success, error, warning, info }
  const { getToken } = useSessionToken();

  const onClick = async () => {
    const token = await getToken();
    const res = await fetch('/api/save', {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}` },
    });
    if (res.ok) toast.success('Saved!');
    else        toast.error('Save failed');
  };

  return <button onClick={onClick}>Save</button>;
}
```

<Warning>
  `<AppBridgeProvider>` takes a **single `config` prop**, not separate
  `apiKey` / `host` props. `useToast()` returns an **object** (`show`,
  `success`, `error`, `warning`, `info`), not a callable function.
</Warning>

The React package ships 30+ hooks — `useAppBridge`, `useToast`,
`useModal`, `useConfirmationModal`, `useResourcePicker`,
`useProductPicker`, `useCollectionPicker`, `useCustomerPicker`,
`useFilePicker`, `useContextualSaveBar`, `useDirtyState`, `useTitleBar`,
`useNavigationMenu`, `useSessionToken`, `useAuthenticatedFetch`,
`useAppQuery`, `useAppMutation`, `useRedirect`, `useLoading`,
`useAppSubscription`, `useAppDispatch`, `useAppDispatchAndWait`,
`useUser`, `useConfig`, `useEnvironment`, `useFeatures`, `useFullscreen`,
`useLeaveConfirmation`, `useUnsavedChanges`, `usePrint`, `useShare`,
`useClipboard`, `useCopyToClipboard`, `useHistory`, `useLifecycle`
(`useOnFocus`, `useOnBlur`, `useOnVisible`, `useOnHidden`), `useCart`.

Full list with signatures: [React Hooks Reference](/app-bridge/react-hooks).

## Resource Picker

The picker action handles the eleven entity types your app might want to
let merchants choose from.

```javascript theme={null}
const result = await app.dispatchAndWait('RESOURCE_PICKER_OPEN', {
  resourceType: 'Product',       // PascalCase or lowercase both accepted
  multiple: true,
  selectionIds: ['prod_abc'],    // pre-selection (preferred shape)
  filter: { query: 'shirt', variants: true },
});

// Response payload: { pickerId, selection } — `selection` is ALWAYS an
// array. Cancel is signalled by an empty selection (there is no
// `cancelled` field on the payload).
if (result.selection.length > 0) {
  console.log(result.selection);
}
```

Or via the helper class for non-promise flows:

```javascript theme={null}
import { ResourcePicker } from '@launchmystore/app-bridge/actions';

ResourcePicker.create(app, { resourceType: 'Customer', multiple: false })
  .subscribe('select', (payload) => console.log(payload.selection))
  .subscribe('cancel', () => console.log('cancelled'))
  .dispatch();
```

### Supported `resourceType` values

| Type            | PascalCase       | lowercase alias   |
| --------------- | ---------------- | ----------------- |
| Product         | `Product`        | `product`         |
| Product variant | `ProductVariant` | `product_variant` |
| Collection      | `Collection`     | `collection`      |
| Customer        | `Customer`       | `customer`        |
| Order           | `Order`          | `order`           |
| Page            | `Page`           | `page`            |
| Blog            | `Blog`           | `blog`            |
| Article         | `Article`        | `article`         |
| File / media    | `File`           | `file`            |
| Metaobject      | `Metaobject`     | `metaobject`      |
| Navigation menu | `Menu`           | `menu`            |

## Session Tokens

```javascript theme={null}
const token = await app.getSessionToken();
```

JWT payload (HS256, signed with your app's client secret):

```json theme={null}
{
  "iss": "https://launchmystore.io",
  "dest": "https://{shop-host}",
  "aud": "your-app-client-id",
  "sub": "merchant-store-id (immutable store UUID)",
  "exp": 1700086400,
  "iat": 1700000000,
  "nbf": 1700000000,
  "sid": "unique-token-id",
  "storeId": "same UUID as sub",
  "domainSlug": "store-slug",
  "shop": "shop host, no scheme",
  "apiKey": "your-app-client-id",
  "permissions": ["read_products", "..."],
  "scopes": ["read_products", "..."]
}
```

Tokens last **24 hours** (`exp = iat + 86400`). The SDK refreshes
automatically when the cached token is within 30s of expiry. The host
endpoint is `POST /api/apps/session-token` proxied through the admin.
Verify tokens on your backend with `jsonwebtoken.verify(token,
clientSecret, { audience, issuer: 'https://launchmystore.io' })` — the
issuer is the full URL, and the per-token id claim is `sid` (not `jti`).
See [Sessions & Authentication](/app-bridge/session-tokens) for the full
claim set and the verification recipe.

## Hosts

App Bridge is the same SDK regardless of where your iframe is mounted —
but the **wired action set** differs per host:

| Host          | Wired actions                                                                                                                                                                                                                                 | Reference                                             |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| Admin         | Modal, ResourcePicker, TitleBar, NavigationMenu, ContextualSaveBar, Loading, LeaveConfirmation, Toast, Redirect, Clipboard, History, Lifecycle, SessionToken, User, Config, Environment, Features, Print, Share, Scanner, Fullscreen, Intents | [Admin extensions](/extensions/admin-blocks)          |
| Checkout      | Cart / Discount / GiftCard / Note / Attribute / Metafield / BuyerJourney + Toast + BRIDGE\_PING + commerce-context reads (`SHIPPING_ADDRESS_GET`, `COST_GET`, …). **No SessionToken, no Redirect.**                                           | [Checkout host](/app-bridge/checkout)                 |
| Post-purchase | Toast + Redirect + ORDER\_GET + CUSTOMER\_GET + CURRENCY\_GET + CLIPBOARD\_WRITE + CART\_LINES\_CHANGE (add only) + DONE + BRIDGE\_PING. **No SessionToken.**                                                                                 | [Post-purchase extensions](/extensions/post-purchase) |

Actions not wired on a host **time out after 10s** rather than throwing
synchronously — so if a checkout iframe dispatches `RESOURCE_PICKER_OPEN`,
the promise rejects ten seconds later with the timeout message.

## Identifying the shop and user

The host pushes an `EXTENSION_CONTEXT` payload to every iframe on mount
(triggered by `APP_BRIDGE_READY`, and on demand via
`EXTENSION_CONTEXT_GET`). In React, read it via `useApi().data`; in vanilla
JS, subscribe to the `EXTENSION_CONTEXT` action or read
`window.__LMS_EXTENSION_CONTEXT__` once the SDK has populated it.

The host sources `shop` and `user` from your `/accounts` row and is
populated for every authenticated admin session.

### Tenant + auth (always present)

| Field        | Source / Default    | What it identifies                                                                                       |
| ------------ | ------------------- | -------------------------------------------------------------------------------------------------------- |
| `domainSlug` | `domainslug` cookie | Tenant key required by every REST call (`?domainSlug=`).                                                 |
| `adminToken` | merchant JWT        | Forwarded merchant JWT for host-side admin endpoints. Prefer App Bridge session tokens for your own API. |

### `shop` (mirrors `app.shop`)

| Field                              | Notes                                                                             |
| ---------------------------------- | --------------------------------------------------------------------------------- |
| `shop.storeId`                     | Immutable store UUID — key app data on this, never on `domainSlug`.               |
| `shop.shopDomain`                  | Full `<slug>.launchmystore.io` host.                                              |
| `shop.primaryDomain`               | Custom domain when connected; `null` otherwise.                                   |
| `shop.hasCustomDomain`             | `true` if the merchant has a published custom domain.                             |
| `shop.shopName`                    | Store / business name. Falls back to `domainSlug`.                                |
| `shop.country`                     | Defaults to `"United States"`.                                                    |
| `shop.currency` / `currencySymbol` | Derived from country by the host.                                                 |
| `shop.locale`                      | Defaults to `"en"`.                                                               |
| `shop.timezone`                    | Reserved — defaults to `"UTC"`; store-level timezone is not yet a settable field. |
| `shop.plan.type`                   | `"Trial" \| "Starter" \| "Gold" \| "Platinum"`.                                   |
| `shop.plan.status`                 | `"active" \| "expired" \| "canceled"`.                                            |
| `shop.plan.expiresAt`              | ISO date.                                                                         |

### `user` (mirrors `app.staffMember`)

| Field           | Notes                                                  |
| --------------- | ------------------------------------------------------ |
| `user.id`       | The merchant or staff user id.                         |
| `user.email`    | Account email.                                         |
| `user.name`     | Owner / staff name.                                    |
| `user.role`     | `"merchant" \| "staff" \| "manager" \| "staff_admin"`. |
| `user.locale`   | Defaults to `"en"`.                                    |
| `user.timezone` | Defaults to `"UTC"`.                                   |

### `app` + slot context

| Field                                                     | Type   | What it identifies                                              |
| --------------------------------------------------------- | ------ | --------------------------------------------------------------- |
| `app.handle`                                              | string | Your app's slug (matches `app.json` `handle`).                  |
| `app.id`                                                  | string | Your app's id.                                                  |
| `app.apiKey`                                              | string | Your app's client id (safe to expose to the iframe).            |
| `target`                                                  | string | Extension placement (e.g. `product.details.block`, `app.home`). |
| `resourceId` / `productId` / `orderId` / `customerId` / … | string | The resource the merchant is looking at.                        |
| `extensionId`                                             | string | Manifest id — echo it back in `APP_BRIDGE_RESIZE`.              |

<Note>
  `domainSlug` + `shop.storeId` are the two fields you should persist data
  against. `domainSlug` can change if a merchant re-keys their store;
  `shop.storeId` is immutable.
</Note>

```ts theme={null}
import { useApi } from '@launchmystore/app-bridge-react';

function ProductBlock() {
  const { data, restApi } = useApi();

  // Identify the shop + user
  const { domainSlug, shop, user } = data;
  console.log(`Running on ${shop?.shopDomain} for ${user?.email}`);

  // Authenticated REST call — restApi wires `Authorization` automatically.
  restApi.get(`/api/v1/my-app/config?domainSlug=${domainSlug}`);
}
```

## Security

<AccordionGroup>
  <Accordion title="Origin checked on both sides">
    The SDK only accepts messages from `atob(config.host)`. The host only
    accepts messages whose `payload.apiKey` matches an installed app. A
    rogue iframe in a different origin cannot forge a message that the
    host will act on.
  </Accordion>

  <Accordion title="Never embed your client secret">
    Session tokens are signed **on the host's server** using the client
    secret stored against your app row. The iframe only ever sees the
    signed JWT — never the secret.
  </Accordion>

  <Accordion title="Iframes are sandboxed">
    Hosts mount iframes with
    `sandbox="allow-scripts allow-forms allow-popups allow-same-origin"`.
    No access to the parent DOM, no access to the parent's cookies — only
    the postMessage channel.
  </Accordion>

  <Accordion title="Validate user input server-side">
    Anything posted from the iframe is in the user's browser. Always
    re-validate on your backend before persisting.
  </Accordion>
</AccordionGroup>

## See Also

* [Actions Reference](/app-bridge/actions) — exhaustive payload shapes.
* [App Bridge for Checkout](/app-bridge/checkout) — cart, discount, note, attribute mutations.
* [App Bridge for Admin](/app-bridge/admin) — modal/picker/title-bar wiring.
* [React Hooks](/app-bridge/react-hooks) — hook-by-hook reference.
* [Sessions & Authentication](/app-bridge/session-tokens) — JWT verification.
