Plugin API
@toposync/plugin-api is the public TypeScript contract between the Toposync frontend host and frontend extensions loaded through Module Federation.
Use this page when you are building the UI side of an extension. For the Python package, entry point, extension.json, static assets, and wheel packaging contract, see Extension authoring.
Current status
The API is already used by first-party Toposync extensions. It is designed to become the stable public contract for third-party frontend extensions, but the external ecosystem has not been fully validated yet.
That means:
- third-party extension authors should treat this as the intended contract, not as a finished marketplace guarantee;
- extensions should target a tested Toposync host version and declare compatibility in their Python
extension.json; - the package is types-first and should be imported with
import typewhenever possible; - bundling, shared dependency, and versioning guidance may be tightened as external extensions are tested outside the monorepo.
What the package covers
@toposync/plugin-api covers frontend integration points only:
- the
ToposyncHostobject passed toactivate(host); - extension registration methods such as settings panels, element types, editor tools, render views, themes, and pipeline operator panels;
- shared UI types for composition elements, notifications, 2D and 3D rendering, i18n, and host-provided UI components;
- URL helpers for Home Assistant ingress, reverse proxies, and non-root deployments.
It does not register the Python extension, serve files, add backend routes, or declare runtime compatibility. Those responsibilities belong to the extension wheel and manifest.
Python extension wheel
-> extension.json declares frontend remote
-> backend exposes /api/extensions
-> frontend host loads remoteEntry.js
-> remote exposes activate(host)
-> extension uses @toposync/plugin-api types
Installation
Install the package with the same peer dependencies used by the Toposync frontend host:
npm install @toposync/plugin-api react react-dom three
For TypeScript projects that render React UI:
npm install -D typescript @types/react @types/react-dom @types/three
For now, target the same minor line as the host you tested against. If your extension is tested with a host using @toposync/plugin-api 0.3.x, declare a compatible peer range such as:
{
"peerDependencies": {
"@toposync/plugin-api": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"three": "^0.167.0"
}
}
Activation contract
The frontend remote must expose the module named in extension.json, usually ./activate. That module must export an activate(host) function.
import type { ToposyncHost } from "@toposync/plugin-api";
export function activate(host: ToposyncHost): void {
host.i18n.registerTranslations({
en: {
"ext.demo.settings.name": "Demo",
},
"pt-BR": {
"ext.demo.settings.name": "Demonstração",
},
});
host.registerSettingsPanel({
id: "com.example.demo.settings",
name: { key: "ext.demo.settings.name", fallback: "Demo" },
render: ({ i18n }) => <div>{i18n.t("ext.demo.settings.name", undefined, "Demo")}</div>,
});
}
activate(host) may return void or a Promise<void>, but it should be deterministic and idempotent from the extension author's perspective. Toposync may reload frontend code during development, and duplicate ids replace previous registrations in most host registries.
Use stable ids. Prefer reverse-DNS or extension-prefixed ids:
com.example.demo.settings
com.example.demo.camera_overlay
com.example.demo.pipeline_panel
Import rules
Most exports are types. Prefer import type so your remote does not bundle unnecessary runtime code:
import type { SettingsPanel, ToposyncHost } from "@toposync/plugin-api";
Use value imports only for runtime helpers:
import { resolveToposyncUrl } from "@toposync/plugin-api";
Do not import from internal Toposync frontend paths such as frontend/src/.... Those are private implementation details and can change without plugin API compatibility guarantees.
Host object
The ToposyncHost object is the extension's entry point into the frontend host:
export type ToposyncHost = {
registerElementType: (elementType: ElementType) => void;
registerNotificationRenderer: (renderer: NotificationRenderer) => void;
registerEditorTool: (tool: EditorTool) => void;
registerFileDropHandler: (handler: FileDropHandler) => void;
registerSettingsPanel: (panel: SettingsPanel) => void;
registerPipelineOperatorPanel: (panel: PipelineOperatorPanel) => void;
registerRenderView: (view: RenderViewDefinition) => void;
registerTheme: (theme: ThemeDefinition) => void;
api: HostApi;
i18n: HostI18n;
ui: HostUi;
};
The registration methods update host-side registries. A later registration with the same id normally replaces the earlier one. This is useful for development reloads, but production extensions should still avoid id collisions.
Internationalization
Register extension translations during activation:
host.i18n.registerTranslations({
en: {
"ext.demo.title": "Demo",
},
"pt-BR": {
"ext.demo.title": "Demonstração",
},
});
Use LocalizedString for host-visible names:
name: {
key: "ext.demo.title",
fallback: "Demo",
}
Inside React components, use the i18n object passed by the host:
function DemoPanel({ i18n }: { i18n: ToposyncHost["i18n"] }) {
const { t } = i18n.useI18n();
return <h2>{t("ext.demo.title", undefined, "Demo")}</h2>;
}
Current locales are en and pt-BR.
URL and base path rules
All browser-visible Toposync URLs must preserve the public base path. This matters for Home Assistant ingress, reverse proxies, and deployments where Toposync is not mounted at /.
Use resolveToposyncUrl() for internal absolute paths:
import { resolveToposyncUrl } from "@toposync/plugin-api";
const response = await fetch(resolveToposyncUrl("/api/demo/items"));
const imageUrl = resolveToposyncUrl(`/files/${encodeURIComponent(directory)}/${encodeURIComponent(file)}`);
For navigation or route generation:
const debugUrl = resolveToposyncUrl(`/streams/debug?${params.toString()}`);
Avoid raw leading-slash paths in browser-visible code:
// Avoid this in extensions.
fetch("/api/demo/items");
window.location.href = "/settings";
getToposyncBasePath() returns the current public base path. Most extensions should use resolveToposyncUrl() instead of concatenating the base path manually.
Settings panels
Settings panels appear in the Toposync settings UI.
import type { SettingsPanel } from "@toposync/plugin-api";
export function createDemoSettingsPanel(): SettingsPanel {
return {
id: "com.example.demo.settings",
name: { key: "ext.demo.settings.name", fallback: "Demo" },
description: { key: "ext.demo.settings.description", fallback: "Demo settings" },
icon: "puzzle-piece",
render: ({ i18n, api, settings, updateSettings }) => {
return (
<button
type="button"
onClick={() => updateSettings({ enabled: !settings.enabled })}
>
{i18n.t("ext.demo.toggle", undefined, "Toggle")}
</button>
);
},
};
}
Use settings and updateSettings for host-managed panel state. Use host.api or extension-specific backend routes for persisted domain data.
Element types
Element types define custom objects that can appear in compositions.
import type { ElementType } from "@toposync/plugin-api";
export const demoElementType: ElementType = {
type: "com.example.demo.marker",
name: { key: "ext.demo.marker.name", fallback: "Demo marker" },
placeable: true,
defaultProps: {
label: "Marker",
},
render2D: ({ ctx, element, viewport }) => {
const p = viewport.worldToScreen({ x: element.position.x, z: element.position.z });
ctx.beginPath();
ctx.arc(p.x, p.y, 8, 0, Math.PI * 2);
ctx.fill();
},
};
Element types can provide:
- 2D canvas rendering;
- 3D
threeobjects; - main map vector rendering;
- markers and effects;
- hit testing and translation behavior;
- action and editor modals.
For 3D rendering, return an Element3DInstance with a dispose() method when your element allocates ThreeJS resources.
Editor tools and file drops
Editor tools add custom interaction modes to the composition editor.
import type { EditorTool } from "@toposync/plugin-api";
export const demoTool: EditorTool = {
id: "com.example.demo.place_marker",
name: { key: "ext.demo.tool.name", fallback: "Place marker" },
icon: "location-dot",
group: {
id: "demo",
name: { key: "ext.demo.tool.group", fallback: "Demo" },
order: 50,
},
order: 10,
createSession: (ctx) => ({
shouldCapturePointer: (event) => event.kind === "down",
onPointerEvent: (event) => {
if (event.kind !== "down") return;
ctx.createElement("com.example.demo.marker", {
name: "Marker",
position: { x: event.world.x, y: 0, z: event.world.z },
});
},
}),
};
File drop handlers let extensions handle dragged files in the editor. Return true when the file was handled so the host does not continue to other handlers.
Pipeline operator panels
Pipeline operator panels customize the configuration UI for backend pipeline operators.
import type { PipelineOperatorPanel } from "@toposync/plugin-api";
export const demoOperatorPanel: PipelineOperatorPanel = {
id: "com.example.demo.operator_panel",
operatorId: "demo.analyze_frame",
render: ({ config, updateConfig, showAdvanced }) => (
<label>
Threshold
<input
type="number"
value={Number(config.threshold ?? 0.5)}
onChange={(event) => updateConfig({ threshold: Number(event.target.value) })}
/>
{showAdvanced ? <span>Advanced options are enabled.</span> : null}
</label>
),
};
The operatorId must match an operator registered by the backend extension. See Pipelines for the execution model.
Notification renderers
Notification renderers customize how extension notifications appear in the UI and optionally in 2D or 3D overlays.
import type { NotificationRenderer } from "@toposync/plugin-api";
export const demoNotificationRenderer: NotificationRenderer = {
id: "com.example.demo.notification",
type: "demo.event",
render: (notification) => (
<div>
<strong>{notification.title}</strong>
{notification.description ? <p>{notification.description}</p> : null}
</div>
),
};
If you create overlays, release resources in dispose() and request host rendering through ctx.requestRender?.() after asynchronous visual changes.
Render views and host UI
Render views add alternate ways to view a composition.
import type { RenderViewDefinition } from "@toposync/plugin-api";
export const demoRenderView: RenderViewDefinition = {
id: "com.example.demo.render_view",
name: { key: "ext.demo.view.name", fallback: "Demo view" },
icon: "diagram-project",
order: 50,
render: ({ compositionName, elements }) => (
<section>
<h2>{compositionName}</h2>
<p>{elements.length} elements</p>
</section>
),
};
The host also exposes reusable UI primitives under host.ui:
Viewport2DReplicarenders a host-managed 2D composition viewport inside extension UI;LiveViewPlayerrenders a camera live view when the host has camera playback support available.
LiveViewPlayer is optional. Always handle the case where it is not present.
Themes
Themes can contribute CSS variables and optional CSS:
import type { ThemeDefinition } from "@toposync/plugin-api";
export const demoTheme: ThemeDefinition = {
id: "com.example.demo.theme",
name: { key: "ext.demo.theme.name", fallback: "Demo theme" },
vars: {
"--surface-primary": "#0e1512",
"--accent-primary": "#62d78b",
},
};
Keep theme CSS scoped and conservative. Avoid global resets or selectors that unintentionally rewrite unrelated host screens.
Host API
host.api currently exposes a small frontend-safe API surface:
type HostApi = {
emitEvent: (eventName: string, payload: unknown, context?: Record<string, unknown>) => Promise<EmitEventResponse>;
getDevice: (deviceId: string) => Promise<{ device_id: string; state: boolean }>;
};
For extension-specific data, expose your own backend routes from the Python extension and call them with resolveToposyncUrl().
Module Federation expectations
The remote must:
- create a browser-compatible
remoteEntry.js; - expose the module declared in
extension.json, usually./activate; - put
remoteEntry.jsand all emitted chunks inside the Python packagestatic/directory; - use a public path strategy that loads chunks next to
remoteEntry.js; - share host-level dependencies as singletons.
Webpack example:
new container.ModuleFederationPlugin({
name: "demo",
filename: "remoteEntry.js",
exposes: {
"./activate": "./src/activate.tsx",
},
shared: {
"@toposync/plugin-api": {
singleton: true,
requiredVersion: "^0.3.0",
},
react: {
singleton: true,
requiredVersion: false,
},
"react-dom": {
singleton: true,
requiredVersion: false,
},
three: {
singleton: true,
requiredVersion: false,
},
},
});
The exact external bundler recipe is still being hardened. First-party extensions use monorepo helpers, but third-party packages should rely only on the public package and documented Module Federation contract.
Compatibility and versioning
Use three layers of compatibility:
peerDependenciesin the frontend package, so package managers warn about incompatible@toposync/plugin-api, React, and ThreeJS versions;requires_core_versioninextension.json, so the backend can skip incompatible extensions before loading them;- distribution tests that install the built wheel into a clean Toposync environment outside the monorepo.
Avoid broad compatibility claims. A third-party extension should document the Toposync host versions it has actually tested.
Stability rules for extension authors
Follow these rules for public extensions:
- import only from
@toposync/plugin-api, not from private Toposync source paths; - wrap internal absolute URLs with
resolveToposyncUrl(); - keep ids stable and globally unique;
- keep React state inside components, not in global module state unless intentional;
- dispose ThreeJS resources, event listeners, timers, and subscriptions;
- tolerate optional host features such as
host.ui.LiveViewPlayer; - do not assume that first-party monorepo build helpers exist in external repositories;
- test the built wheel after
pip install, not only in the source checkout.
What still needs validation
Before Toposync can call third-party frontend extensions fully supported, we still need to validate:
- an external template repository using
@toposync/plugin-apifrom npm; - webpack and Vite remote builds outside the monorepo;
- clean wheel installs with remote chunks, CSS, images, and source maps;
- upgrade behavior across Toposync minor versions;
- stronger diagnostics when remote loading or activation fails;
- clearer security guidance for backend routes, credentials, and frontend asset loading.