Sandbox Extensions
Sandbox Extensions
Sandbox extensions run your JavaScript inside an isolated iframe embedded in the merchant dashboard. Unlike JSON extensions (which are declarative), sandbox extensions give you full programmatic control — you can make API calls, open resource pickers, manage complex state, and render dynamic UI.
The @selorax/ui SDK handles all communication with the host dashboard via postMessage. You never interact with postMessage directly unless you need the low-level protocol.
Getting Started
Install the SDK:
npm install @selorax/uiWrite your extension entry file:
const { selorax, Card, Stack, Text, Button } = require("@selorax/ui");
// Signal readiness to the host
selorax.ready();
// Build a UI tree and send it to the host for rendering
selorax.render(
Card(
{ title: "My Extension" },
Stack(
{ direction: "vertical", gap: "md" },
Text({ content: "Hello from a sandbox extension!" }),
Button({
label: "Load Products",
variant: "primary",
action: { type: "navigate", url: "/products" },
}),
),
),
);Configure it in selorax.config.json:
{
"extensions": [
{
"extension_id": "my-sandbox-ext",
"target": "order.detail.block",
"title": "My Extension",
"mode": "sandbox",
"entry": "extensions/my-ext/index.js"
}
]
}Deploy:
selorax deployThe CLI builds your entry file with esbuild (bundled, minified, IIFE format), uploads the bundle, and registers it with the platform.
The selorax Global Object
When @selorax/ui is loaded, it attaches a selorax object to window. This is the primary interface for communicating with the host dashboard.
Properties
| Property | Type | Description |
|---|---|---|
selorax.context | object | Read-only context from the host: store_id, order_id, product_id, customer_id, user, locale, etc. |
selorax.settings | object | Merchant-configured settings for this extension |
selorax.extension | object | Extension metadata: id, name, version, etc. |
Context and settings are populated automatically when the host sends the selorax:init message. They are available by the time your code runs (the SDK sets up the listener before your code executes).
Lifecycle Methods
| Method | Description |
|---|---|
selorax.ready() | Signal to the host that the extension has loaded and is ready. Must be called. |
selorax.close() | Request the host to close/unmount this extension. |
Rendering
| Method | Description |
|---|---|
selorax.render(ui) | Send a UI tree (single node or array) to the host for rendering. |
selorax.render(
Card({ title: "Status" }, Text({ content: "Everything is good." })),
);You can call render() multiple times to update the UI. Each call replaces the previous tree.
Feedback
| Method | Description |
|---|---|
selorax.toast(message, type?) | Show a toast notification. type: 'success' (default), 'error', 'warning' |
selorax.toast("Saved successfully!");
selorax.toast("Something went wrong", "error");Navigation
| Method | Description |
|---|---|
selorax.navigate(url) | Navigate within the dashboard. url is a dashboard-relative path. |
selorax.openLink(url) | Open an external URL in a new browser tab. |
selorax.navigate("/orders");
selorax.openLink("https://docs.myapp.com");Modal Control
| Method | Description |
|---|---|
selorax.modal.open(options) | Open a modal. options: { title?: string, ui: UINode } |
selorax.modal.close() | Close the currently open modal. |
selorax.modal.open({
title: "Confirm Deletion",
ui: Stack(
{ direction: "vertical", gap: "md" },
Text({ content: "Are you sure you want to delete this item?" }),
Stack(
{ direction: "horizontal", gap: "sm" },
Button({
label: "Cancel",
variant: "secondary",
action: { type: "close_modal" },
}),
Button({
label: "Delete",
variant: "destructive",
action: { type: "close_modal" },
}),
),
),
});Action Dispatch
| Method | Description |
|---|---|
selorax.dispatchAction(actionType, payload?) | Dispatch any action to the host dashboard. |
selorax.dispatchAction("navigate", { url: "/orders/123" });
selorax.dispatchAction("open_modal", { id: "confirm-dialog" });API Access
The selorax.api namespace proxies HTTP requests through the host dashboard. Requests are authenticated with the merchant's session and scoped to their store. Your app's granted OAuth scopes are enforced server-side.
| Method | Signature | Description |
|---|---|---|
selorax.api.get | get(path): Promise<any> | GET request |
selorax.api.post | post(path, body?): Promise<any> | POST request |
selorax.api.put | put(path, body?): Promise<any> | PUT request |
selorax.api.patch | patch(path, body?): Promise<any> | PATCH request |
selorax.api.delete | delete(path): Promise<any> | DELETE request |
Paths are relative to the platform API base (e.g., /apps/v1/orders). You do not include the domain.
// Fetch orders
const orders = await selorax.api.get(
"/apps/v1/orders?status=confirmed&limit=10",
);
console.log(orders.data);
// Update a product
await selorax.api.put("/apps/v1/products/150", {
name: "Updated Product Name",
});Rate limits
API proxy requests are rate-limited to 60 requests per minute per installation. Exceeding the limit returns a 429 status.
Resource Pickers
Pickers open a native dashboard dialog for selecting resources. They return a Promise that resolves with the selection (or null if cancelled).
| Method | Signature | Description |
|---|---|---|
selorax.picker.product | product(options?): Promise<any[] | null> | Open the product picker |
selorax.picker.customer | customer(options?): Promise<any[] | null> | Open the customer picker |
selorax.picker.order | order(options?): Promise<any[] | null> | Open the order picker |
Options:
| Option | Type | Default | Description |
|---|---|---|---|
multiple | boolean | false | Allow selecting multiple items |
// Single product
const products = await selorax.picker.product();
if (products) {
console.log("Selected:", products[0].name);
}
// Multiple customers
const customers = await selorax.picker.customer({ multiple: true });
if (customers) {
console.log("Selected", customers.length, "customers");
}App Bridge UI
Control dashboard chrome elements from your extension.
Title Bar
selorax.ui.titleBar({
title: "Inventory Manager",
breadcrumbs: [{ label: "Apps" }, { label: "Inventory Manager" }],
primaryAction: { label: "Save" },
secondaryActions: [{ label: "Export" }, { label: "Settings" }],
});Save Bar
Show a contextual save bar when the user has unsaved changes:
selorax.ui.showSaveBar({
message: "Unsaved changes",
saveAction: { label: "Save", loading: false },
discardAction: { label: "Discard" },
});
// Listen for save/discard clicks
selorax.onSaveBar((event) => {
if (event.action === "save") {
// Persist changes
selorax.ui.hideSaveBar();
} else if (event.action === "discard") {
// Revert changes
selorax.ui.hideSaveBar();
}
});Loading Bar
selorax.ui.startLoading();
// After your operation completes:
selorax.ui.completeLoading();Title Bar Actions
Listen for clicks on title bar action buttons:
selorax.onTitleBarAction((event) => {
if (event.action_type === "primary") {
// Primary action clicked (e.g., "Save")
} else if (event.action_type === "secondary") {
// Secondary action clicked; event.index tells you which one
}
});Metafields
Read and write custom data attached to store resources.
| Method | Signature | Description |
|---|---|---|
selorax.metafields.get | get(resourceType, resourceId): Promise<object[]> | Get all metafield values for a resource |
selorax.metafields.getValue | getValue(namespace, key, resourceType, resourceId): Promise<object | null> | Get a single metafield value |
selorax.metafields.set | set(data): Promise<object> | Set a metafield value |
selorax.metafields.setMany | setMany(metafields): Promise<object> | Set multiple metafield values |
selorax.metafields.remove | remove(namespace, key, resourceType, resourceId): Promise<void> | Delete a metafield value |
// Read metafields for an order
const metafields = await selorax.metafields.get(
"order",
selorax.context.order_id,
);
// Set a metafield
await selorax.metafields.set({
namespace: "my-app",
key: "risk_score",
resource_type: "order",
resource_id: selorax.context.order_id,
value: 85,
});
// Get a specific value
const score = await selorax.metafields.getValue(
"my-app",
"risk_score",
"order",
selorax.context.order_id,
);See the Metafields guide for full API details.
Billing
Manage charges and subscriptions from within your extension.
| Method | Signature | Description |
|---|---|---|
selorax.billing.createCharge | createCharge(data): Promise<object> | Create a one-time charge |
selorax.billing.createSubscription | createSubscription(data): Promise<object> | Create a recurring subscription |
selorax.billing.getActive | getActive(): Promise<object[]> | Get active charges and subscriptions |
selorax.billing.getCharge | getCharge(chargeId): Promise<object> | Get a charge by ID |
selorax.billing.createUsageCharge | createUsageCharge(data): Promise<object> | Create a metered usage charge |
selorax.billing.getWalletBalance | getWalletBalance(): Promise<object> | Get wallet balance |
selorax.billing.debitWallet | debitWallet(data): Promise<object> | Debit from wallet |
const charge = await selorax.billing.createCharge({
name: "Premium Report",
amount: 500,
currency: "BDT",
});Webhooks
Manage webhook subscriptions from your extension.
| Method | Signature | Description |
|---|---|---|
selorax.webhooks.list | list(): Promise<object[]> | List all webhook subscriptions |
selorax.webhooks.subscribe | subscribe(data): Promise<object> | Create a webhook subscription |
selorax.webhooks.unsubscribe | unsubscribe(subscriptionId): Promise<void> | Delete a webhook subscription |
const subs = await selorax.webhooks.list();
await selorax.webhooks.subscribe({
topic: "order.status_changed",
url: "https://myapp.com/webhooks/orders",
});Internationalization
The SDK includes a lightweight i18n system:
selorax.i18n.setTranslations({
en: {
greeting: "Hello, {{name}}!",
status: { pending: "Pending", confirmed: "Confirmed" },
},
bn: {
greeting: "হ্যালো, {{name}}!",
status: { pending: "অপেক্ষমান", confirmed: "নিশ্চিত" },
},
});
// selorax.i18n.locale is set from the host context automatically
const msg = selorax.i18n.t("greeting", { name: "Rahim" });
// "Hello, Rahim!" (if locale is 'en')
const status = selorax.i18n.t("status.pending");
// "Pending"Context Updates
Register a listener for when the host context changes (e.g., the user navigates to a different order):
const unsubscribe = selorax.onContextUpdate((context) => {
console.log("New context:", context);
// Re-render with new data
});
// Later, to stop listening:
unsubscribe();Component Builder Functions
The @selorax/ui package exports builder functions for all 74 components. Each function returns a plain { type, props, children } object. Components that accept children take them as trailing arguments.
const { Card, Stack, Text, Button, Badge, Table } = require("@selorax/ui");
// Containers accept children as arguments
const ui = Card(
{ title: "Orders" },
Stack(
{ direction: "vertical", gap: "md" },
Text({ content: "Recent orders" }),
Table({
columns: [
{ key: "id", label: "Order ID" },
{ key: "status", label: "Status" },
{ key: "total", label: "Total" },
],
rows: [
{ id: "#1045", status: "Confirmed", total: "1,460 BDT" },
{ id: "#1044", status: "Pending", total: "860 BDT" },
],
}),
),
);
selorax.render(ui);The full list of builder functions matches the JSON extension components.
PostMessage Protocol (Advanced)
If you prefer not to use the SDK, or need to implement a client in a different language, here is the raw postMessage protocol.
Messages from Extension to Host
| Message Type | Payload | Description |
|---|---|---|
selorax:ready | {} | Extension loaded and ready |
selorax:render | { ui } | Send UI tree for rendering |
selorax:toast | { message, toast_type } | Show toast notification |
selorax:navigate | { url } | Client-side navigation |
selorax:open_link | { url } | Open external link |
selorax:modal_open | { title, ui } | Open a modal |
selorax:modal_close | {} | Close modal |
selorax:action | { action, ...payload } | Dispatch action |
selorax:api_request | { request_id, method, url, body? } | API proxy request |
selorax:picker_request | { request_id, resource_type, multiple? } | Open resource picker |
selorax:ui_titlebar | { config } | Set title bar |
selorax:ui_save_bar_show | { config } | Show save bar |
selorax:ui_save_bar_hide | {} | Hide save bar |
selorax:ui_loading_start | {} | Start loading bar |
selorax:ui_loading_complete | {} | Complete loading bar |
selorax:close | {} | Request unmount |
Messages from Host to Extension
| Message Type | Payload | Description |
|---|---|---|
selorax:init | { context, settings, extension } | Initialization with context and settings |
selorax:context_update | { context } | Context changed |
selorax:api_response | { request_id, success, data?, error? } | API response |
selorax:picker_result | { request_id, selection } | Picker selection result |
selorax:ui_save_bar_action | { action } | Save bar button clicked (save or discard) |
selorax:ui_titlebar_action | { action_type, index? } | Title bar action clicked |
All messages are { type: 'selorax:xxx', ...payload }. The SDK uses request_id to correlate async requests with their responses (30-second timeout).
Security Model
Sandbox extensions run inside iframes with strong isolation:
- Origin validation — The host dashboard validates the origin of all incoming postMessage events. In production, only messages from the registered
sandbox_urldomain are processed. - HTTPS only — Sandbox URLs must use HTTPS. HTTP URLs are rejected during deployment.
- Signed session tokens — Each iframe session receives a JWT (5-minute TTL, HS256) signed with your app's
session_signing_key. The token containsstore_id,installation_id,extension_id, andapp_id. - Scope enforcement — API proxy requests are checked against your app's granted OAuth scopes. A
read:ordersscope lets youGET /ordersbut notPOST /orders. - Rate limiting — 60 API proxy requests per minute per installation. Exceeding the limit returns HTTP 429.
- SSRF prevention —
call_backendURLs are validated against your app's registered domain. Private/internal network addresses are blocked. - Path traversal prevention — API proxy paths are decoded and checked for
..sequences.
Full Example: Product Tag Manager
This extension adds a "Custom Tags" block to the product detail page. It loads existing metafield tags, lets the merchant add/remove tags, and saves changes.
const {
selorax,
Card,
BlockStack,
InlineStack,
Text,
TextField,
Button,
Tag,
Spinner,
} = require("@selorax/ui");
let tags = [];
let newTag = "";
let loading = true;
selorax.ready();
// Load existing tags from metafields
async function loadTags() {
loading = true;
render();
try {
const value = await selorax.metafields.getValue(
"tag-manager",
"custom_tags",
"product",
selorax.context.product_id,
);
tags = value && value.value ? JSON.parse(value.value) : [];
} catch {
tags = [];
}
loading = false;
render();
}
async function saveTags() {
await selorax.metafields.set({
namespace: "tag-manager",
key: "custom_tags",
resource_type: "product",
resource_id: selorax.context.product_id,
value: JSON.stringify(tags),
});
selorax.toast("Tags saved!");
}
function render() {
if (loading) {
selorax.render(
Card({ title: "Custom Tags" }, Spinner({ label: "Loading tags..." })),
);
return;
}
const tagNodes = tags.map((tag, i) =>
Tag({
content: tag,
onRemove: { type: "set_state", key: "__remove_tag_" + i, value: true },
}),
);
selorax.render(
Card(
{ title: "Custom Tags" },
BlockStack(
{ gap: "md" },
InlineStack({ gap: "sm" }, ...tagNodes),
InlineStack(
{ gap: "sm" },
TextField({
name: "new_tag",
placeholder: "Add a tag...",
bind: "__new_tag",
}),
Button({
label: "Add",
variant: "secondary",
action: { type: "set_state", key: "__add_tag", value: true },
}),
),
Button({
label: "Save Tags",
variant: "primary",
action: { type: "set_state", key: "__save", value: true },
}),
),
),
);
}
// Use the product picker
async function pickRelatedProduct() {
const selected = await selorax.picker.product({ multiple: false });
if (selected && selected.length > 0) {
selorax.toast("Selected: " + selected[0].name);
}
}
loadTags();What's Next
- CLI Reference — Build, validate, and deploy extensions with the CLI
- Metafields — Attach custom data to store resources
- Merchant Settings — Define configurable settings
- App Bridge — Lower-level postMessage protocol for full-page embedded apps