Storeware Docs

App Bridge

How a page the admin frames talks to the admin. The handshake, toasts, the title bar, the save bar, modals, confirm, the resource picker and navigation.

App Bridge is the set of messages between a page of yours that the admin frames and the admin around it. Your home, a panel and an action's window all speak it.

  • createAppBridge from @storeware/plugin-sdk/bridge speaks it for you, in any framework or none.
  • @storeware/app-bridge-react wraps that in a provider and hooks.
  • The messages are zod schemas in @storeware/plugin-sdk/bridge/protocol, the same ones the admin checks every message against.

Early access

The CLI and the SDK are not on npm yet, so the commands below do not run for everyone. If you would like to build an app now, tell us.

Connect

import { createAppBridge } from '@storeware/plugin-sdk/bridge';

const bridge = createAppBridge({ hostOrigin: 'https://app.storeware.ai' });

bridge.applyTheme(document.documentElement);
bridge.autoResize(document.body);

const welcome = await bridge.ready;
console.log(welcome.point, welcome.context.productId);

hostOrigin is the admin's origin. Every message goes to it and nothing from any other origin is believed. Read it from your configuration, so the same code works against a local admin.

Anything you send before the admin answers is held and sent after, so a page can call bridge.toast on its first line.

The handshake

Your page says hello with the versions it speaks, and the admin answers welcome with everything the page needs to start:

FieldWhat it is
versionThe version agreed: 2
appYour app's name and slug
storeIdThe shop the merchant is in
pointWhere the page is framed: a point such as admin.order.detail.sidebar, or admin.app.home for your home and its modals
contextThe record beside the page, such as productId, and on a list's action, selection
themeThe admin's 13 --sw-* variables and its scheme
sessionA session token and when it expiresAt
pickersWhich resource pickers the merchant's grant lets you open
modalWhether the page is in a window, where modal.close works

A page says hello each time it loads. When your frame follows one of its own links, the new page starts clean: whatever the last page put in the title bar, the loading bar or the save bar is cleared.

Requests and answers

A message with an id is answered with a response of the same id: ok: true with a result, or ok: false with an error of { code, message }. The SDK turns a refusal into a BridgeError with its code.

CodeWhy
invalidThe message did not match its schema. The message names the field.
not_readySent before the handshake finished.
rate_limitedMore than 3 toasts in 10 seconds.
not_grantedA picker for records your grant cannot read.
unavailableNot offered where the page is: a save bar on a page without one, a modal inside a modal, modal.close outside one.
busyThe merchant is already answering one of your app's questions.
failedThe admin tried and could not, such as minting a session.

The SDK waits 10 seconds for the admin's answer (timeoutMs changes it). A modal, a confirm and a picker wait for the merchant, as long as they take.

Session tokens

const response = await fetch('/api/reviews', {
  headers: { authorization: `Bearer ${await bridge.sessionToken()}` },
});

sessionToken returns the current token while it has more than a minute left, and asks the admin for a fresh one after. The admin also sends a fresh one every four minutes. On your server, authenticateAdmin reads the bearer token as it reads ?session= on the first load.

Toasts

bridge.toast('Review published');
bridge.toast('That review could not be published', 'error');

A toast is shown with your app's name in front of it, so a merchant never mistakes it for Storeware's. It is cut at 200 characters, and a page may show 3 in any 10 seconds.

Loading bar

bridge.loading(true);
await importReviews();
bridge.loading(false);

A thin bar across the top of your frame, for work the merchant is waiting on. It stands still for anybody who has asked for reduced motion.

Title bar

bridge.titleBar({
  title: 'Reviews',
  primary: { id: 'publish', label: 'Publish' },
  secondary: [{ id: 'export', label: 'Export' }],
});

bridge.on('title_bar.action', (id) => {
  if (id === 'publish') publish();
  if (id === 'export') exportReviews();
});

A heading and buttons above your frame, drawn by the admin. One primary button and up to 3 secondary ones, each with disabled and destructive if you need them. Send it again to change it, or {} to clear it.

Save bar

bridge.saveBar.dirty(true);

bridge.on('save_bar.save', async () => {
  try {
    await save();
    bridge.saveBar.result(true);
  } catch {
    bridge.saveBar.result(false, 'The name is missing');
  }
});

bridge.on('save_bar.discard', () => resetForm());

While your page is dirty the admin's save bar shows, and leaving the page asks the merchant first, as it does for Storeware's own forms. Save saves every form on the page, yours included: the admin sends save_bar.save and waits up to 30 seconds for your result. A failure keeps your changes unsaved and names your app in the bar.

The save bar is on your home, the shop's settings, and the product and customer pages. Anywhere else, and inside a window, save_bar is answered unavailable.

Modals

const data = await bridge.modal.open('/reviews/new', 'New review', { size: 'large' });

Opens a page of your home in a window over the admin, and resolves when it closes. The path is inside your [app] url, so your app needs a home to open one. Inside the window, the page closes itself:

bridge.modal.close({ saved: true });

data is what the window's page passed, up to 16 KB of JSON, or undefined when the merchant closed it. A window cannot open another window.

An action at an .action point opens in a window too, so modal.close closes it when the action is done.

Confirm

const sure = await bridge.confirm({
  title: 'Delete 3 reviews?',
  message: 'They are taken off the shop at once.',
  confirmLabel: 'Delete',
  destructive: true,
});

The admin asks in its own dialog, titled with your app's name ("Reviews asks"), and resolves true or false.

Resource picker

const products = await bridge.pick('products', { multiple: true, max: 10 });
// [{ id: '…', title: 'Stoneware mug', subtitle: 'stoneware-mug' }]

The merchant searches their own records and chooses. You are given each one's id, title and sometimes a subtitle, and an empty list when they cancel. multiple lets them choose up to max, at most 50.

A picker opens only for records your grant already reads:

ResourceNeeds
productsproducts:read
variantsproducts:read
collectionsproducts:read
customerscustomers:read

Holding the write permission counts. Anything else is refused with not_granted; welcome.pickers lists what you may open.

bridge.navigate('/orders?status=open');
bridge.appNavigate('/settings');
bridge.redirect('https://help.example.com/reviews');
  • navigate opens a page of this shop's admin, by its path inside the shop.
  • appNavigate opens a page of your home.
  • redirect leaves Storeware. The admin shows the merchant where it goes first, and opens it in a new tab when they agree. https only, or http on localhost.

Paths are relative, start with one / and never contain ... If the page has unsaved changes, the merchant is asked before leaving.

Without the SDK

The protocol is plain postMessage. Every version 2 message carries protocol: 'storeware.bridge':

const HOST = 'https://app.storeware.ai';
const post = (message) => parent.postMessage({ protocol: 'storeware.bridge', ...message }, HOST);

addEventListener('message', (event) => {
  if (event.origin !== HOST) return;
  if (event.data?.protocol !== 'storeware.bridge') return;
  if (event.data.type === 'welcome') {
    post({ type: 'toast', message: `Hello, shop ${event.data.storeId}` });
  }
});

post({ type: 'hello', id: 'hello-1', versions: [2] });

Messages

From your page to the admin:

TypeFieldsAnswered with
helloid, versionswelcome
session.requestid{ token, expiresAt }
resizeheight
toastmessage, tone (info or error)
loadingon
title_bartitle, primary, secondary
save_bardirty
save_bar.resultok, message
modal.openid, path, title, size{ data }, once closed
modal.closedata
confirmid, title, message, confirmLabel, destructive{ confirmed }
pickerid, resource, multiple, max{ selection }
navigatepath
app.navigatepath
redirecturl

Any of them may carry an id, and is then answered. From the admin to your page: welcome, response, session (a fresh token), theme, title_bar.action, save_bar.save and save_bar.discard.

Version 1

Pages written for version 1 (protocol: 'storeware.bridge.v1', sent by connectToHost in the SDK) keep working. The admin answers ready with context and theme, and still takes resize, toast and navigate. Version 1 has no handshake and none of the rest, so new pages should use version 2.

On this page