@stackable-labs/sdk-extension-host
v1.16.0
Published
Host-side SDK for embedding Stackable extensions.
Readme
@stackable-labs/sdk-extension-host
Host-side SDK for creating an app that allows embeddable slots via Stackable extensions.
Installation
npm install @stackable-labs/sdk-extension-hostPeer dependencies
react >= 18.0.0 < 19.0.0
react-dom >= 18.0.0 < 19.0.0Usage
Wrap your app with ExtensionProvider and place ExtensionSlot wherever extension UI should appear:
import { ExtensionProvider, ExtensionSlot } from '@stackable-labs/sdk-extension-host';
function App() {
return (
<ExtensionProvider
extensions={extensions}
capabilityHandlers={handlers}
components={{ 'ui-button': Button, 'ui-text': Text }}
>
<ExtensionSlot target="slot.header" />
<MainContent />
<ExtensionSlot target="slot.content" />
</ExtensionProvider>
);
}Key exports
ExtensionProvider— React context provider; manages extension sandboxesExtensionSlot— renders extension-produced UI into a named host slotregisterComponents— register allowed host components extensions may renderCapabilityRPCHandler— handles capability requests from extension sandboxesSandboxManager— creates and destroys hidden iframe sandboxes per extension
Building a local preview host
During extension development, you can create a lightweight preview host app in your extension repo to test your extension against the real SDK sandbox — no private monorepo required.
Structure
packages/
├── extension/ ← your extension
└── preview/ ← local preview host
└── src/
├── main.tsx
├── App.tsx
└── mockData.jsonPreview App.tsx pattern
import { ExtensionProvider, ExtensionSlot } from '@stackable-labs/sdk-extension-host';
import type { CapabilityHandlers } from '@stackable-labs/sdk-extension-host';
import { hostComponents } from '@stackable-labs/embeddables/components';
import type {
ApiRequest,
ExtensionRegistryEntry,
Permission,
ToastPayload,
} from '@stackable-labs/sdk-extension-contracts';
import manifestRaw from '../../extension/public/manifest.json';
import mockData from './mockData.json';
const manifest = {
...manifestRaw,
permissions: manifestRaw.permissions as Permission[],
};
const extensions: ExtensionRegistryEntry[] = [
{
id: manifest.name.toLowerCase().replace(/\s+/g, '-'),
manifest,
bundleUrl: `http://localhost:${import.meta.env.VITE_EXTENSION_PORT || '6543'}`,
enabled: true,
},
];
const mockContext = { customerId: 'cust_123', customerEmail: '[email protected]' };
const capabilityHandlers: CapabilityHandlers = {
'data.query': async (payload: ApiRequest) => {
// return mock data based on payload.action
return mockData;
},
'actions.toast': async (payload: ToastPayload) => {
console.log('[Preview] toast:', payload);
},
'context.read': async () => mockContext,
};
export default function App() {
return (
<ExtensionProvider
extensions={extensions}
components={hostComponents()}
capabilityHandlers={capabilityHandlers}
>
{manifest.targets.map((target) => (
<ExtensionSlot key={target} target={target} context={mockContext} />
))}
</ExtensionProvider>
);
}Wiring two dev servers with Turborepo
At the workspace root, add:
.env
VITE_EXTENSION_PORT=6543
VITE_PREVIEW_PORT=6544turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalEnv": ["VITE_EXTENSION_PORT", "VITE_PREVIEW_PORT"],
"tasks": {
"dev": { "cache": false, "persistent": true }
}
}Run both servers with one command:
pnpm devExtension runs at :6543, preview host at :6544. The preview host loads the extension bundle from the dev server, so edits to the extension hot-reload in the preview automatically.
See stackable-extensions-commerce-orders and stackable-extensions-approvals for complete working examples.
Changelog
See npm version history.
License
SEE LICENSE IN LICENSE
