Getting Started

Plugin Architecture

How the iframe model, App Bridge protocol, and security sandbox work together.

The Iframe Model

Every Whatalo plugin runs inside a sandboxed <iframe> in the admin. The iframe's src points to your server — your plugin loads there, isolated from the admin's JavaScript context.

The sandbox policy is:

sandbox="allow-scripts allow-forms allow-same-origin"
  • allow-scripts — your JavaScript runs normally
  • allow-forms — form submissions work
  • allow-same-origin — your plugin can access its own cookies and localStorage (on your domain, not the admin's)

allow-same-origin is safe here because the Same-Origin Policy blocks cross-origin access to the admin's storage. The exception: Whatalo actively rejects plugins whose appUrl shares the same origin as the admin dashboard — that configuration would bypass sandbox isolation.

Modal iframes (opened via bridge.modal.open(...)) use a stricter sandbox — allow-scripts allow-forms only, no allow-same-origin.

The App Bridge Protocol

Communication between your plugin and the admin uses the browser's window.postMessage API. The @whatalo/plugin-sdk/bridge package handles the protocol details so you work with clean React hooks.

There are four message types:

Message typeDirectionPurpose
whatalo:initHOST → PLUGINHandshake — sends the admin's origin to the plugin
whatalo:actionPLUGIN → HOSTPlugin requests an action (toast, navigate, resize, billing...)
whatalo:contextHOST → PLUGINHost sends store/user/theme data to the plugin
whatalo:ackHOST → PLUGINHost confirms an action was received and processed

Handshake Lifecycle

iframe loads


HOST sends whatalo:init { origin: "https://admin.whatalo.com" }

    ▼ (plugin stores parent origin for all future postMessage calls)
PLUGIN sends whatalo:action { action: "ready" }


HOST sends whatalo:context { storeId, storeName, user, theme, currentPage, ... }


Plugin renders — useWhataloContext().isReady === true

The whatalo:init handshake is critical for security: without it, the plugin would have to use postMessage("*", ...) which broadcasts to any listener. After receiving whatalo:init, the plugin targets only the admin origin for all outbound messages.

Context Data

Once the handshake completes, your plugin receives a WhataloContext object:

interface WhataloContext {
  storeId: string;       // Public store identifier (NOT the internal UUID)
  storeName: string;     // Store display name
  user: {
    id: string;
    name: string;
    email: string;
    role: "owner" | "admin" | "editor" | "staff" | "viewer";
  };
  appId: string;         // Your plugin's ID from the manifest
  currentPage: string;   // Active page path (e.g., "settings")
  locale: string;        // Store locale (e.g., "es", "en")
  theme: "light" | "dark"; // Current admin color scheme
  initialHeight: number; // Suggested starting height in pixels
  orderId?: string;      // Set when opened from order context
  productId?: string;    // Set when opened from product context
}

storeId is the public identifier — never the internal database UUID.

Page Routing

There is no client-side router. The admin tells your plugin which page to display by passing currentPage in the context. Your plugin reads this value and renders the matching component:

const context = useWhataloContext();

switch (context.currentPage) {
  case "settings":
    return <SettingsPage />;
  case "dashboard":
  default:
    return <DashboardPage />;
}

The currentPage value matches the path field of the page entry in adminUI.pages in your manifest.

Theme Sync

The admin watches for dark/light mode changes using a MutationObserver on document.documentElement. When the admin's theme changes, it sends an updated whatalo:context with the new theme value. Your plugin receives it, and the useWhataloContext() hook re-renders with the new theme.

The template includes useThemeSync() which applies the theme to the plugin's document root automatically.

Available Actions

Actions are requests your plugin sends to the admin host. Each action is acknowledged with a whatalo:ack message containing { success: boolean, error?: string }. Actions time out after 5 seconds if no ack is received.

ActionWhat it does
toastShows a notification banner in the admin (3 per 10s per plugin)
navigateNavigates the admin to a path (5 per 10s)
modal.openOpens a modal overlay loading a URL from your domain (3 per 10s)
modal.closeCloses the currently open modal
resizeUpdates the iframe height (30 per 10s)
billing.*Billing operations — plans, subscriptions, switches (10 per 10s)

Rate limits are enforced with a sliding window per action type, synchronized across browser tabs via BroadcastChannel.

Security Model

Origin Validation

The host validates the origin of every inbound message against the plugin's registered appUrl. Messages from any other origin are silently dropped. This prevents other iframes or scripts from impersonating your plugin.

Same-Origin Blocking

If your appUrl shares the same origin as the admin (e.g., both served from the same domain), the admin renders an error and refuses to load the iframe. This is a hard safety rule — it prevents the sandbox from being bypassed.

The navigate action only accepts paths starting with /store/ or /admin/. Plugins cannot navigate the admin to external URLs or to arbitrary admin routes that might expose sensitive UI.

Modal URLs must be:

  1. Valid absolute URLs (http: or https: only)
  2. From your plugin's origin (same scheme + host + port)
  3. Not using dangerous protocols (javascript:, data:, blob:, file:)

Localhost URLs are also allowed in development mode, but only in development.

Rate Limiting

Each action type has a per-plugin rate limit enforced with a sliding 10-second window. Rate limits are also synchronized across tabs using BroadcastChannel so opening your plugin in multiple tabs does not multiply the limit.

Resize and Scroll

Plugins report their content height to the host via the resize action. The admin sets the iframe height to match, then manages scrolling at the page level — which creates a native, seamless scroll feel instead of a scrollbar inside the iframe.

The template's useAutoResize() hook does this automatically using a ResizeObserver on your root element.

Next Steps

On this page