UI contributions
Integration developers can build a full native React home page, or supply a worker-rendered page using the extension kit. Native pages own their content, layout, forms, and settings. The common host keeps navigation, plan access, and uninstall consistent.
Worker bundles contribute UI by returning component specs — plain
JSON like { type: "Pill", props: { … } } — from slot renderers. The
host validates every tree and renders it with first-party markup, so
contributions inherit the product’s typography, spacing, and theming
automatically. Worker contributions use the kit’s tones and layouts.
| Slot id | Called with | Must return |
|---|---|---|
photo-tile.badge |
{ workspaceId, projectId, assetIds[], api } — once per photo grid |
A map of assetId → Badge | ProgressRing | null |
project.header.status |
{ workspaceId, projectId, api } |
Pill or null |
dashboard.card |
{ workspaceId, api } |
Card or null |
integration.page |
{ workspaceId, canConfigure, api } |
Any validated kit component tree, or null |
activity.feed |
— (no renderer) | Declared in the catalog; rows are emitted by your sync, not rendered by the bundle. See Activity events. |
Root component types are enforced per slot — a Card returned to the
pill slot is a validation failure for that render, not a broken page.
Your integration’s home page
Section titled “Your integration’s home page”Each installed integration has a page under Integrations → My integrations. The host provides the integration identity, install status, store link, and permission-controlled uninstall action. The body can use either of the following developer page options.
Native React page
Section titled “Native React page”Trusted first-party integrations register a React component in
apps/web-next/src/components/integrations/IntegrationManagementContent.tsx.
That module exports INTEGRATION_MANAGEMENT_PAGES and
IntegrationManagementPageProps. Each app’s component controls its full
page body: sections, tabs, forms, settings, and other product components.
The host keeps the integration identity, access notices, navigation, and
uninstall action around it.
For example, a native Promotions home can compose its existing editable campaign settings with its own layout:
import type { ComponentType } from "react";import { PromotionsClient } from "@/app/org/[namespace]/(authed)/settings/promotions/PromotionsClient";
function CustomPromotionsHome({ canConfigure, canReadSettings,}: IntegrationManagementPageProps) { return ( <div className="space-y-6"> <header> <h2 className="text-xl font-semibold">Campaign workspace</h2> <p className="text-sm text-muted-foreground"> Manage offers and your public pricing calculator. </p> </header> {canReadSettings ? ( <PromotionsClient embedded readOnly={!canConfigure} /> ) : ( <p>Ask a workspace administrator to view campaign settings.</p> )} </div> );}
// In IntegrationManagementContent.tsx, preserve the other app mappings:export const INTEGRATION_MANAGEMENT_PAGES: Readonly<Record<string, ComponentType<IntegrationManagementPageProps>>> = { // ...other integration pages promotions: CustomPromotionsHome, };For another integration, add its catalog entry, create its React page,
and add an app-id mapping to this registry. The generic
integrations/installed/[appId] route does not need app-specific changes.
Leave integration.page out of the manifest when using a native page;
an explicit worker page declaration takes precedence over the registry.
The props include the current listing, plan eligibility, configuration
and settings-read access, and onChanged. Use those values to decide
which settings to show, and call onChanged() after a successful change
to refresh installed state. Server endpoints still enforce the plan and
permissions. Native pages can keep saved information readable while
hiding configuration controls from free or unauthorized members.
Worker page contribution
Section titled “Worker page contribution”A bundled extension declares integration.page in the integration’s
catalog manifest and registers a renderer for it:
// In your registered IntegrationApp catalog entry:contributions: { bundle: "stageory-first-party://my-integration/extension.js", slots: ["integration.page"], extensionApiVersion: 1,}import { Card, Stack, Text, register, startExtensionRuntime, type IntegrationPageContext,} from "@homestage/extension-kit";
register("integration.page", async (ctx: IntegrationPageContext) => Stack({ gap: "md" }, [ Card({ title: "Overview" }, [ Text({ text: "Your integration is installed in this workspace." }), Text({ text: ctx.canConfigure ? "You have permission to configure this integration." : "Ask a workspace administrator to change its configuration.", variant: "caption", }), ]), ]),);
startExtensionRuntime(self);The canonical URL is
/org/:namespace/integrations/installed/:appId. The page appears in the
sidebar after installation, including installations that still need setup.
An integration must be installed and have an eligible plan or active trial
before its extension body runs. Members without configuration permission
receive canConfigure: false. Free and expired workspaces can still open
the host’s installed-integration page, inspect retained install status,
and uninstall when their role permits it.
canConfigure is a presentation hint, not an authorization mechanism.
Every server endpoint must check the member’s permissions and workspace
plan independently. Uninstall stays in the host so an extension cannot
hide or replace that action.
This is a component-spec page, with the same bounds as other slots.
It does not accept React, HTML, CSS, arbitrary URLs, or embedded frames.
The current kit contains presentation components and buttons; it has no
text inputs, selects, forms, navigation API, or write bridge. api.get
can only call existing endpoints under /v1/integrations/:appId/.
Declaring a page does not create those endpoints. A custom configuration
form therefore needs a native first-party page or a separately reviewed
extension-kit and server capability addition.
Bundle resolution currently supports registered
stageory-first-party://... bundles served by Stageory. Declaring an
external HTTPS bundle does not enable community loading. The built-in
integrations use native home components unless their manifest explicitly
declares integration.page.
Components
Section titled “Components”All constructors come from @homestage/extension-kit and return
ComponentSpec JSON.
Badge({ shape: "dot" | "count", tone, tooltip?, count? })ProgressRing({ value: /* 0–1, or null = indeterminate */, tone, tooltip? })Pill({ text, tone, icon? })Card({ title }, children)Row({ gap?: "sm" | "md" }, children)Stack({ gap?: "sm" | "md" }, children)Text({ text, variant?: "body" | "caption", truncate? })Icon({ name })Button({ label, onPress })"neutral" | "success" | "pending" | "warning" | "danger" — mapped to
theme-consistent colors on the host side, correct in light and dark
mode. This is the only way a contribution expresses color.
A fixed nine-name set, rendered from the product’s own icon system:
images · calendar · folder · plug · send · users · receipt · megaphone · chart
Unknown names fall back to plug rather than failing the render.
Buttons and events
Section titled “Buttons and events”onPress never crosses the worker boundary. The spec carries only a
generated handler id; when the customer clicks, the host routes the
event back into your worker and your callback runs there. A callback
that throws is contained — it cannot crash in-flight renders.
The api bridge
Section titled “The api bridge”ctx.api.get(path) is your only network access. It resolves page-side
with the customer’s session, against an allowlist scoped to your app —
your worker never sees a token, a cookie, or an unlisted endpoint.
ctx.api.theme.mode ("light" | "dark") is sampled fresh for every
render.
Hard limits
Section titled “Hard limits”| Limit | Value | On overflow |
|---|---|---|
| Render soft budget | 100 ms | logged warning |
| Render hard budget | 1 000 ms | worker terminated + restarted, render fails |
| Nodes per slot tree | 200 | that render fails |
| Tree depth | 8 | that render fails |
| Response size | 64 KB | that render fails |
| Crash strikes | 3 per 10 min | UI disabled for the session |
Compact slots render failures as a small muted glyph with a tooltip. Integration pages show a skeleton while loading and an inline retry message on failure; a crash-loop-disabled page asks the member to reload. The host’s page controls remain available.