Building Extensions

Project Structure

Understanding how a Stackable extension project is organized helps you navigate the monorepo, locate key files, and follow conventions for surfaces, state management, and capability registration.

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"]
}
FieldPurpose
nameDisplay name shown to users
versionSemver version string
targetsSurface slots the extension renders into
permissionsCapabilities the extension uses
allowedDomainsHostnames 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.

Auto-generated from Stackable Extension SDK. Questions/Issues? developers@stackablelabs.com

Previous
Styling & Theming
Project Structure | Stackable Labs :. Dev Documentation