Skip to main content

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

npm install @launchmystore/app-bridge
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.

Quick Start

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);
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.

Core API

The App instance returned by createApp() exposes four methods:
MethodReturnsUse 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()

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

dispatchAndWait()

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 with Error('App Bridge: <action> failed: <error>').
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()

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

// Later
unsubscribe();

getSessionToken()

const token = await app.getSessionToken();

const response = await fetch('/api/orders', {
  headers: { Authorization: `Bearer ${token}` },
});
Tokens are cached and auto-refreshed when within 30s of expiry — call getSessionToken() before every authenticated request and the SDK takes care of caching. See Sessions & Authentication.

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 table are the authoritative source of truth.

SDK-exposed action families

FamilyActionsHosts that wire it
ToastTOAST_SHOW, TOAST_DISMISSAdmin, Checkout, Post-purchase
ModalMODAL_OPEN, MODAL_CLOSE, MODAL_PRIMARY_ACTION, MODAL_SECONDARY_ACTIONAdmin
ResourcePickerRESOURCE_PICKER_OPEN, RESOURCE_PICKER_CLOSE, RESOURCE_PICKER_SELECTION, RESOURCE_PICKER_CANCELAdmin
TitleBarTITLE_BAR_UPDATE, TITLE_BAR_PRIMARY_ACTION, TITLE_BAR_SECONDARY_ACTIONAdmin
NavigationMenuNAVIGATION_MENU_UPDATE, NAVIGATION_MENU_NAVIGATEAdmin
ContextualSaveBarCONTEXTUAL_SAVE_BAR_SHOW, CONTEXTUAL_SAVE_BAR_HIDE, CONTEXTUAL_SAVE_BAR_SAVE, CONTEXTUAL_SAVE_BAR_DISCARDAdmin
LoadingLOADING_START, LOADING_STOPAdmin
LeaveConfirmationLEAVE_CONFIRMATION_ENABLE, LEAVE_CONFIRMATION_DISABLEAdmin
SessionTokenSESSION_TOKEN_REQUESTAdmin, Checkout, Post-purchase
ClipboardCLIPBOARD_WRITE, CLIPBOARD_READ_REQUESTAdmin
HistoryHISTORY_PUSH, HISTORY_REPLACE, HISTORY_GO, HISTORY_BACK, HISTORY_FORWARDAdmin
RedirectREDIRECTAdmin, Checkout, Post-purchase
LifecycleLIFECYCLE_FOCUS, LIFECYCLE_BLUR, LIFECYCLE_VISIBLE, LIFECYCLE_HIDDENAdmin
BridgeBRIDGE_PINGAll hosts
Cart / Buyer-journey (checkout-only)CART_GET, CART_LINES_CHANGE, DISCOUNT_CODE_CHANGE, GIFT_CARD_CHANGE, NOTE_CHANGE, ATTRIBUTE_CHANGE, CHECKOUT_TOTALS_GET, CUSTOMER_GET, CURRENCY_GET, ORDER_NOTE_SET, COUPON_APPLY_REQUEST, BUYER_JOURNEY_INTERCEPT, BUYER_JOURNEY_PROCEEDCheckout
Post-purchase (subset)CART_LINES_CHANGE (add only), ORDER_GET, CUSTOMER_GET, CURRENCY_GET, REDIRECT, DONEPost-purchase
Other SDK action classes — User, Print, Share, Scanner, Fullscreen, Config, Environment, Features — exist in the SDK and will dispatch postMessages, but no host currently handles them. They will resolve via dispatch() (fire-and-forget) but a dispatchAndWait() call will reject with a 10-second timeout. They are reserved for future host implementations.
Counts vary as the surface grows. See:

Wire Format

The protocol is small enough to implement without the SDK if you need to. The iframe posts:
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:
{
  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:
window.parent.postMessage({
  type: 'APP_BRIDGE_RESIZE',
  extensionId: '<your manifest handle>',  // required so the host knows which iframe to resize
  height: document.body.scrollHeight,
}, '*');
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.

React Integration

The React adapter wraps the same SDK in a context provider plus a hook for every action family.
npm install @launchmystore/app-bridge @launchmystore/app-bridge-react
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>;
}
<AppBridgeProvider> takes a single config prop, not separate apiKey / host props. useToast() returns an object (show, success, error, warning, info), not a callable function.
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.

Resource Picker

The picker action handles the eleven entity types your app might want to let merchants choose from.
const result = await app.dispatchAndWait('RESOURCE_PICKER_OPEN', {
  resourceType: 'Product',       // PascalCase or lowercase both accepted
  multiple: true,
  initialSelectionIds: [{ id: 'prod_abc' }],
  filter: { query: 'shirt', variants: true },
});

if (!result.cancelled) {
  console.log(result.selection); // array of selected entities
}
Or via the helper class for non-promise flows:
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

TypePascalCaselowercase alias
ProductProductproduct
Product variantProductVariantproduct_variant
CollectionCollectioncollection
CustomerCustomercustomer
OrderOrderorder
PagePagepage
BlogBlogblog
ArticleArticlearticle
File / mediaFilefile
MetaobjectMetaobjectmetaobject
Navigation menuMenumenu

Session Tokens

const token = await app.getSessionToken();
JWT payload (HS256, signed with your app’s client secret):
{
  "iss": "https://launchmystore.io",
  "aud": "your-app-client-id",
  "sub": "merchant-store-id (immutable store UUID)",
  "exp": 1700003600,
  "iat": 1700000000,
  "nbf": 1700000000,
  "sid": "unique-token-id"
}
Tokens last one hour. The SDK refreshes automatically when the cached token is within 30s of expiry. The host endpoint is GET /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 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:
HostWired actionsReference
AdminModal, ResourcePicker, TitleBar, NavigationMenu, ContextualSaveBar, Loading, LeaveConfirmation, Toast, Redirect, Clipboard, History, Lifecycle, SessionTokenAdmin extensions
CheckoutCart / Discount / GiftCard / Note / Attribute / BuyerJourney + Toast + SessionTokenCheckout host
Post-purchaseToast + Redirect + ORDER_GET + CUSTOMER_GET + CURRENCY_GET + CART_LINES_CHANGE (add only) + DONE + SessionTokenPost-purchase extensions
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)

FieldSource / DefaultWhat it identifies
domainSlugdomainslug cookieTenant key required by every REST call (?domainSlug=).
adminTokenmerchant JWTForwarded merchant JWT for host-side admin endpoints. Prefer App Bridge session tokens for your own API.

shop (mirrors app.shop)

FieldNotes
shop.storeIdImmutable store UUID — key app data on this, never on domainSlug.
shop.shopDomainFull <slug>.launchmystore.io host.
shop.primaryDomainCustom domain when connected; null otherwise.
shop.hasCustomDomaintrue if the merchant has a published custom domain.
shop.shopNameStore / business name. Falls back to domainSlug.
shop.countryDefaults to "United States".
shop.currency / currencySymbolDerived from country by the host.
shop.localeDefaults to "en".
shop.timezoneReserved — 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.expiresAtISO date.

user (mirrors app.staffMember)

FieldNotes
user.idThe merchant or staff user id.
user.emailAccount email.
user.nameOwner / staff name.
user.role"merchant" | "staff" | "manager" | "staff_admin".
user.localeDefaults to "en".
user.timezoneDefaults to "UTC".

app + slot context

FieldTypeWhat it identifies
app.handlestringYour app’s slug (matches app.json handle).
app.idstringYour app’s id.
app.apiKeystringYour app’s client id (safe to expose to the iframe).
targetstringExtension placement (e.g. product.details.block, app.home).
resourceId / productId / orderId / customerId / …stringThe resource the merchant is looking at.
extensionIdstringManifest id — echo it back in APP_BRIDGE_RESIZE.
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.
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

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.
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.
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.
Anything posted from the iframe is in the user’s browser. Always re-validate on your backend before persisting.

See Also