Install
Example
The whole loop: the page asks the bridge for an ID token, sends it to your server, and your server verifies it with@tassyir/app before it
calls the Admin API.
The page (your manifest’s admin.page):
@tassyir/app README
has the Express version with a token store):
const who = await tassyir.verifyIdToken(idToken) gives userId,
businessId, installationId, sessionId and locale.
Who is looking
Only the ID token says who is looking. The dashboard loads your page atadmin.page, plus the app path and the dashboard’s own query string. Tassyir
vouches for nothing in that address. Never read a business, a user or a store
from it to decide what to show or allow. context is for display only. To
learn who is looking, send idToken()’s token to your server and verify it
there with @tassyir/app (verifyIdToken, or authenticate to also get an
Admin API token). A token that has not been verified proves nothing.
The dashboard never sends your page a Tassyir credential. The ID token is the
only one, it is minted for your app and this installation, and it expires
after 60 seconds.
Calls
Every call posts one message to the dashboard. It resolves with the payload of the dashboard’s reply, or rejects with anAppBridgeError. Payloads are
the ones the Tassyir App Bridge protocol defines. The SDK checks them before
posting, so most mistakes are refused without a round trip.
idTokenmints a new token on every call; the SDK never caches one. Send each token to your server once.toast.show:toneisinfo(default),successorerror. The dashboard signs the toast with your app’s name and showsmessageas text: control and bidi characters become spaces, white space is folded, and it is cut to 150 characters. More than 2,000 characters is refused.modal.openshows one of your pages (path, the app-path rules below) in a dashboard modal and resolves when the modal closes.titlefollows the toast rules and is cut to 80 characters;""is refused,nullmeans none.sizeissmall,medium(default) orlarge. The modal’s page may callidToken,toast.show,modal.close,resource.pickandcontext.getonly; anything else isNOT_ALLOWED_HERE.- App paths (
navigate,modal.open) are relative toadmin.pageand start with one/:/reviews/2?tab=sent. No scheme or host, no backslash, white space, control or bidi character, fragment, or.,..or empty segment (percent-encoded ones too). At most 1,024 characters before the?, 2,048 in all, and a query of at most 1,024 characters once written the wayURLSearchParamswrites it. navigate({ path })says your app moved to another of its pages. The frame is not reloaded (show the page yourself, for example with your router); the dashboard’s address follows, as a new history entry or, withreplace: true, in place.navigate({ dashboard })leaves your app:home,orders,order(withid),customers,customer(withid),tracking,inventory,products(the current store’s) orapps.saveBar.showshows the dashboard’s save bar, busy whilesavingis true. Its buttons sendsaveBar.saveandsaveBar.discard. It stays untilsaveBar.hide, and it does not stop the merchant from leaving the page.titleBar.setsets your page’s title beside the app’s name;nullor""clears it.resource.pickopens the dashboard’s picker forproduct,collectionorcustomerand resolves when the merchant closes it:selectionis empty when cancelled, one item unlessmultiple, 50 at most. Products are{ id, name, image }, collections{ id, name }, customers{ id, provinceCode }, plusnameandphoneonly when the installation holdsread_customer_contact. It needsread_products(products, collections) orread_customers(customers) in your manifest.
modal.open and resource.pick wait for the merchant as long as the dialog
stays open. Every other call times out after 10 seconds (timeoutMs per
bridge or per call), and any call takes an AbortSignal (signal).
Events and context
{ locale: 'ar' | 'en', dir: 'rtl' | 'ltr', theme: 'light' | 'dark', storeId: string | null }. storeId is the dashboard’s current store,
or null when there is none or your installation is limited to other stores.
bridge.context.current holds the latest one (null until the dashboard has
sent it), and context.changed listeners hear real changes only.
How the bridge talks to the dashboard
- Messages go to
https://app.tassyir.ioonly, never*. A message is taken only when it comes from the page’s parent window and from that origin. Replies are matched to calls by id. - Ready.
createAppBridge()says hello at once (acontext.get), which is what makes the dashboard count the page as loaded, and it repeats it with a back-off until the dashboard answers: a message posted before the dashboard listens is lost. Calls made meanwhile wait and are sent in order once it answers. - Leaving the page. On
pagehidethe bridge clears the title and hides the save bar (titleBar.set({ title: null }),saveBar.hide()), so neither outlives the page that set them. It does not when the page is kept in the back/forward cache, because the whole dashboard is kept with it. - Limits. The dashboard counts calls per installation over a sliding
minute and refuses more with
RATE_LIMITED. Past 240 messages a minute it drops them without answering. The bridge counts what this page sends and refuses (RATE_LIMITED,source: 'sdk') instead of posting what the dashboard would refuse or drop.
A page that makes more than 30 requests a minute to your server cannot ask
for a token per request: batch the requests, or have your server open its own
short session once it has verified a token.
Errors
Every refusal is anAppBridgeError with:
code: stable; act on it, never onmessage;call: the call that failed;source:dashboard(the dashboard’s reply) orsdk(refused or failed before an answer);path: the payload field at fault, for the SDK’s ownINVALID_PAYLOAD;messagefor people, andfix, one sentence saying what to do.
isAppBridgeError(error, code?) narrows an unknown error.
Options
dashboardOrigin unset in production. Set it only to run your page
inside a dashboard on your machine or on staging, and never from the page’s
address. It must be an exact origin (https, or http on localhost); * is
refused.
bridge.destroy() stops listening and rejects waiting calls with
DESTROYED. bridge.embedded is false when the page is not in a frame (every
call then rejects NOT_EMBEDDED), and bridge.connected turns true once the
dashboard has answered.
Size
About 7 kB gzipped once minified (17.7 kB minified, 32 kB as shipped). Half of it is the error messages and fix sentences.Not here yet
React hooks (a provider,useAppBridge, useBridgeContext) come in a later
release. bridge.context.current and bridge.on('context.changed', …) are
built for useSyncExternalStore meanwhile.
Checking against the dashboard
src/frontend-crosscheck.test.ts runs the dashboard’s own bridge host code
(lib/apps/bridge-protocol.ts in the private tassyir/frontend repository)
against what this package sends: the constants, about 24,000 payloads, and
every call end to end. CI cannot reach that repository, so it runs locally:
origin/main (or TASSYIR_FRONTEND_REF) with git show; run
git -C ../frontend fetch origin first.