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:
- In response to
app-bridge:ready(initial token delivery) - 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.originon 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
401responses. Do not cache tokens beyond their TTL. - Never trust data from untrusted origins. Discard messages where
event.origindoes 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:
| Claim | Description |
|---|---|
iss | Issuer: the SeloraX dashboard URL |
dest | Destination: your app's URL |
aud | Audience: your app's client_id |
sub | Subject: the store_id (as a string) |
sid | Session ID: the installation_id (as a string) |
app_id | Your app's numeric ID |
jti | Unique token identifier (UUID) |
iat | Issued at (Unix timestamp) |
exp | Expiry (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:
- Extension Platform Overview — Architecture and targets
- Sandbox Extensions — Iframe-based extensions with the
@selorax/uiSDK (richer postMessage protocol with 16+ message types) - JSON Extensions — Declarative UI with no iframe needed
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 Type | Direction | Payload | Purpose |
|---|---|---|---|
app-bridge:ready | App to Dashboard | { type } | Signal iframe loaded, request initial token |
selorax:request-session-token | App to Dashboard | { type } | Request fresh token (e.g., on 401) |
selorax:billing-redirect | App to Dashboard | { type, url } | Navigate dashboard to billing page |
selorax:session-token | Dashboard to App | { type, token } | Deliver JWT session token |