SeloraXDEVELOPERS

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/ui

Write 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 deploy

The 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

PropertyTypeDescription
selorax.contextobjectRead-only context from the host: store_id, order_id, product_id, customer_id, user, locale, etc.
selorax.settingsobjectMerchant-configured settings for this extension
selorax.extensionobjectExtension 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

MethodDescription
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

MethodDescription
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

MethodDescription
selorax.toast(message, type?)Show a toast notification. type: 'success' (default), 'error', 'warning'
selorax.toast("Saved successfully!");
selorax.toast("Something went wrong", "error");
MethodDescription
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");
MethodDescription
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

MethodDescription
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.

MethodSignatureDescription
selorax.api.getget(path): Promise<any>GET request
selorax.api.postpost(path, body?): Promise<any>POST request
selorax.api.putput(path, body?): Promise<any>PUT request
selorax.api.patchpatch(path, body?): Promise<any>PATCH request
selorax.api.deletedelete(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).

MethodSignatureDescription
selorax.picker.productproduct(options?): Promise<any[] | null>Open the product picker
selorax.picker.customercustomer(options?): Promise<any[] | null>Open the customer picker
selorax.picker.orderorder(options?): Promise<any[] | null>Open the order picker

Options:

OptionTypeDefaultDescription
multiplebooleanfalseAllow 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.

MethodSignatureDescription
selorax.metafields.getget(resourceType, resourceId): Promise<object[]>Get all metafield values for a resource
selorax.metafields.getValuegetValue(namespace, key, resourceType, resourceId): Promise<object | null>Get a single metafield value
selorax.metafields.setset(data): Promise<object>Set a metafield value
selorax.metafields.setManysetMany(metafields): Promise<object>Set multiple metafield values
selorax.metafields.removeremove(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.

MethodSignatureDescription
selorax.billing.createChargecreateCharge(data): Promise<object>Create a one-time charge
selorax.billing.createSubscriptioncreateSubscription(data): Promise<object>Create a recurring subscription
selorax.billing.getActivegetActive(): Promise<object[]>Get active charges and subscriptions
selorax.billing.getChargegetCharge(chargeId): Promise<object>Get a charge by ID
selorax.billing.createUsageChargecreateUsageCharge(data): Promise<object>Create a metered usage charge
selorax.billing.getWalletBalancegetWalletBalance(): Promise<object>Get wallet balance
selorax.billing.debitWalletdebitWallet(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.

MethodSignatureDescription
selorax.webhooks.listlist(): Promise<object[]>List all webhook subscriptions
selorax.webhooks.subscribesubscribe(data): Promise<object>Create a webhook subscription
selorax.webhooks.unsubscribeunsubscribe(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 TypePayloadDescription
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 TypePayloadDescription
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_url domain 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 contains store_id, installation_id, extension_id, and app_id.
  • Scope enforcement — API proxy requests are checked against your app's granted OAuth scopes. A read:orders scope lets you GET /orders but not POST /orders.
  • Rate limiting — 60 API proxy requests per minute per installation. Exceeding the limit returns HTTP 429.
  • SSRF preventioncall_backend URLs 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