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"]
}
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