Building Extensions
Project Structure
Understanding the layout and entry points helps you organize code and configure what your extension needs. This page covers the standard structure, key configuration files, and SDK patterns for building extensions.
Directory Layout
my-extension/
packages/
extension/ # Your extension code
public/
manifest.json # Extension manifest
src/
index.tsx # Entry point
store.ts # Shared state (createStore)
surfaces/ # Surface components
Header.tsx # slot.header
Content.tsx # slot.content
Footer.tsx # slot.footer
components/ # Feature components
lib/ # API wrappers, utilities
preview/ # Local preview host — DO NOT MODIFY
package.json # Workspace root
tsconfig.json
Key Files
manifest.json
The extension manifest declares what the extension needs from the framework:
{
"name": "My Extension",
"version": "0.1.0",
"targets": ["slot.header", "slot.content", "slot.footer"],
"permissions": ["context:read", "data:fetch"],
"allowedDomains": ["api.example.com"]
}
| Field | Purpose |
|---|---|
name | Display name shown to users |
version | Semver version string |
targets | Surface slots the extension renders into |
permissions | Capabilities the extension uses |
allowedDomains | Hostnames for data.fetch requests (exact hostnames or *.<suffix> wildcards) |
See External APIs > Wildcards for the full allowedDomains rules, including the apex-vs-subdomain distinction and what's rejected at format vs. submission time.
index.tsx — Entry Point
The entry point bootstraps the extension runtime:
import { createExtension } from '@stackable-labs/sdk-extension-react'
import { Header } from './surfaces/Header'
import { Content } from './surfaces/Content'
import { Footer } from './surfaces/Footer'
const Extension = () => (
<>
<Header />
<Content />
<Footer />
</>
)
// NOTE: extensionId is optional — used when connected to a registered extension
createExtension(() => <Extension />, { extensionId: 'my-extension' })
createExtension handles:
- Sandboxed iframe communication with the framework
- Capability injection (makes
useCapabilities()work) - Surface registration (maps
<Surface id="...">to layout slots in the embedding application)
Do not modify index.html — the extension always bootstraps through createExtension.
store.ts — Shared State
The store provides cross-surface state management:
import { createStore, useStore, Surface, ui } from '@stackable-labs/sdk-extension-react'
// store.ts
type ViewState = { type: 'list' } | { type: 'detail'; id: string }
interface AppState {
viewState: ViewState
}
export const appStore = createStore<AppState>({
viewState: { type: 'list' },
})
// Content.tsx
export function Content(): React.ReactElement {
const viewState = useStore(appStore, (s) => s.viewState)
const goToDetail = (id: string) => appStore.set({ viewState: { type: 'detail', id } })
const goBack = () => appStore.set({ viewState: { type: 'list' } })
return (
<Surface id="slot.content">
{(viewState.type === 'list') && (
<ui.Button onClick={() => goToDetail('abc123')}>View Detail</ui.Button>
)}
{(viewState.type === 'detail') && (
<ui.Stack direction="column" gap="2">
<ui.Text>Viewing: {viewState.id}</ui.Text>
<ui.Button onClick={goBack}>Back</ui.Button>
</ui.Stack>
)}
</Surface>
)
}
All surfaces import from the same store instance. Use useStore(appStore, selector) to read state with minimal re-renders.
Surface Files
Each surface is a React component in src/surfaces/:
import { Surface, ui } from '@stackable-labs/sdk-extension-react'
export function Content() {
return (
<Surface id="slot.content">
<ui.Card>
<ui.CardContent>
<ui.Text>Extension content</ui.Text>
</ui.CardContent>
</ui.Card>
</Surface>
)
}
The id prop must match a target in manifest.json.
Preview Host
The packages/preview/ directory contains a local preview host that simulates the production environment. Do not modify this directory — it is pre-configured and managed by the SDK tooling.
Import Conventions
All SDK imports come from two packages:
// Runtime: hooks, components, utilities
import { ui, Surface, createExtension, useCapabilities, createStore, useStore, useContextData }
from '@stackable-labs/sdk-extension-react'
// Types and constants (dev-time only)
import type { ExtensionManifest } from '@stackable-labs/sdk-extension-contracts'
Components use the ui.* namespace — do not import components directly.