SeloraXDEVELOPERS

App Bridge

App Bridge

The App Bridge is a postMessage-based protocol for communication between embedded app iframes and the SeloraX merchant dashboard. It handles session token delivery, token refresh, and navigation commands.

How It Works

When a merchant opens your app in their dashboard, SeloraX loads your app inside an iframe. The App Bridge establishes a secure communication channel between that iframe and the parent dashboard window using the browser's postMessage API.

+-----------------------+       postMessage        +-------------------+
|  SeloraX Dashboard    | <======================> |  Your App (iframe) |
|  (parent window)      |   app-bridge:ready        |                   |
|                       |   session-token           |                   |
|                       |   billing-redirect        |                   |
+-----------------------+                          +-------------------+

Messages (App to Dashboard)

app-bridge:ready

Your app must send this message as soon as the iframe has loaded. The dashboard responds by sending the initial session token.

window.parent.postMessage({ type: "app-bridge:ready" }, "*");

This is the handshake that kicks off the entire session. Without it, the dashboard will not deliver a token.

selorax:request-session-token

Request a fresh session token. Send this when your app receives a 401 Unauthorized response from the platform API, indicating the current token has expired.

window.parent.postMessage({ type: "selorax:request-session-token" }, "*");

The dashboard will generate a new token and send it back via the selorax:session-token message.

selorax:billing-redirect

Navigate the parent dashboard to a billing page, such as the charge approval screen. This is used when your app needs the merchant to approve a payment.

window.parent.postMessage(
  {
    type: "selorax:billing-redirect",
    url: "/22/settings/apps/billing/5",
  },
  "*",
);

The url value is a relative path within the dashboard. The dashboard will navigate to that route.

Messages (Dashboard to App)

selorax:session-token

The dashboard sends this message in two scenarios:

  1. In response to app-bridge:ready (initial token delivery)
  2. In response to selorax:request-session-token (token refresh)
window.addEventListener("message", (event) => {
  if (event.data.type === "selorax:session-token") {
    const token = event.data.token;
    // Use token for API calls: Authorization: Bearer <token>
  }
});

The token is a JWT signed with your app's session_signing_key, valid for 10 minutes.

Security

  • Always validate event.origin on both sides. In production, check that the origin matches the expected dashboard or app URL before processing any message.
  • Session tokens expire in 10 minutes. Request fresh tokens before expiry or on 401 responses. Do not cache tokens beyond their TTL.
  • Never trust data from untrusted origins. Discard messages where event.origin does not match your expected source.
  • Do not embed sensitive data in postMessage payloads beyond what the protocol requires.

Reference Implementation

A minimal App Bridge client class you can adapt for your app:

class SeloraXAppBridge {
  constructor() {
    this.token = null;
    this.listeners = new Map();
    window.addEventListener("message", this.handleMessage.bind(this));
  }
 
  handleMessage(event) {
    // Validate origin in production
    // if (event.origin !== 'https://admin.selorax.io') return;
 
    const { type, token } = event.data;
 
    if (type === "selorax:session-token") {
      this.token = token;
      this.emit("token", token);
    }
  }
 
  ready() {
    window.parent.postMessage({ type: "app-bridge:ready" }, "*");
  }
 
  requestToken() {
    window.parent.postMessage({ type: "selorax:request-session-token" }, "*");
  }
 
  billingRedirect(url) {
    window.parent.postMessage({ type: "selorax:billing-redirect", url }, "*");
  }
 
  getToken() {
    return this.token;
  }
 
  on(event, callback) {
    if (!this.listeners.has(event)) this.listeners.set(event, []);
    this.listeners.get(event).push(callback);
  }
 
  emit(event, data) {
    (this.listeners.get(event) || []).forEach((cb) => cb(data));
  }
}

Usage

const bridge = new SeloraXAppBridge();
 
bridge.on("token", (token) => {
  console.log("Got session token:", token);
  // Initialize your app, set auth headers, fetch data, etc.
});
 
// Kick off the handshake
bridge.ready();

Handling Token Expiry

Wrap your API calls to automatically request a fresh token on 401:

async function apiFetch(url, options = {}) {
  const headers = {
    ...options.headers,
    Authorization: `Bearer ${bridge.getToken()}`,
    "Content-Type": "application/json",
  };
 
  let response = await fetch(url, { ...options, headers });
 
  if (response.status === 401) {
    // Request a new token and wait for it
    const newToken = await new Promise((resolve) => {
      bridge.on("token", resolve);
      bridge.requestToken();
    });
 
    headers["Authorization"] = `Bearer ${newToken}`;
    response = await fetch(url, { ...options, headers });
  }
 
  return response;
}

Session Token Format

The session token is a JWT signed with your app's session_signing_key using HMAC-SHA256. It contains:

ClaimDescription
issIssuer: the SeloraX dashboard URL
destDestination: your app's URL
audAudience: your app's client_id
subSubject: the store_id (as a string)
sidSession ID: the installation_id (as a string)
app_idYour app's numeric ID
jtiUnique token identifier (UUID)
iatIssued at (Unix timestamp)
expExpiry (10 minutes after iat)

Server-Side Verification

Your backend should verify the session token when receiving requests from the iframe:

const crypto = require("crypto");
 
function verifySessionToken(token, signingKey) {
  const [headerB64, payloadB64, signatureB64] = token.split(".");
 
  // Verify HMAC-SHA256 signature
  const data = `${headerB64}.${payloadB64}`;
  const expected = crypto
    .createHmac("sha256", signingKey)
    .update(data)
    .digest("base64url");
 
  if (expected !== signatureB64) {
    throw new Error("Invalid session token signature");
  }
 
  const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
 
  // Check expiry
  if (payload.exp < Date.now() / 1000) {
    throw new Error("Session token expired");
  }
 
  return {
    app_id: payload.app_id,
    store_id: payload.sub, // store_id is in the "sub" claim
    installation_id: payload.sid, // installation_id is in the "sid" claim
    client_id: payload.aud, // client_id is in the "aud" claim
  };
}

Alternatively, use the platform verification endpoint:

curl -X POST "https://api.selorax.io/api/apps/session/verify" \
  -H "Content-Type: application/json" \
  -d '{
    "session_token": "<token>",
    "client_id": "sx_app_...",
    "client_secret": "sx_secret_..."
  }'

Error Handling

Token delivery timeout

If the dashboard doesn't respond to app-bridge:ready within a reasonable time (e.g. 5 seconds), show a fallback UI:

let tokenReceived = false;
 
bridge.on("token", () => {
  tokenReceived = true;
});
bridge.ready();
 
setTimeout(() => {
  if (!tokenReceived) {
    showError("Unable to connect to dashboard. Please reload the page.");
  }
}, 5000);

Invalid or missing origin

Always validate event.origin in production to prevent other windows from sending fake messages:

handleMessage(event) {
  const ALLOWED_ORIGINS = [
    'https://admin.selorax.io'
  ];
  if (!ALLOWED_ORIGINS.includes(event.origin)) return;
 
  // Process message...
}

Token refresh race condition

If multiple API calls fail with 401 simultaneously, avoid requesting multiple tokens. Use a promise lock:

let refreshPromise = null;
 
async function getRefreshedToken() {
  if (refreshPromise) return refreshPromise;
 
  refreshPromise = new Promise((resolve) => {
    bridge.on("token", (token) => {
      refreshPromise = null;
      resolve(token);
    });
    bridge.requestToken();
  });
 
  return refreshPromise;
}

Extensions vs. Embedded Apps

The App Bridge described on this page is for full-page embedded apps that run as iframes within the dashboard. If you want to build smaller UI blocks that appear on specific pages (order detail, product detail, dashboard widgets, etc.), use the extension system instead:

Sandbox extensions use a similar postMessage approach but with a more comprehensive protocol including API proxy, resource pickers, modal/drawer control, title bar, save bar, and more.

Message Reference

Message TypeDirectionPayloadPurpose
app-bridge:readyApp to Dashboard{ type }Signal iframe loaded, request initial token
selorax:request-session-tokenApp to Dashboard{ type }Request fresh token (e.g., on 401)
selorax:billing-redirectApp to Dashboard{ type, url }Navigate dashboard to billing page
selorax:session-tokenDashboard to App{ type, token }Deliver JWT session token