Surfaces and Lifecycle

A ui.surface expose exports a mount function. The host calls it with a container owned by the current realm and a TapFederatedSurfaceMountContext for the exact package release, installation, contribution, and instance.

import type {
  TapFederatedSurfaceMount,
  TapFederatedSurfaceMountContext,
} from '@theaiplatform/miniapp-sdk/surface';

export function mount(
  container: HTMLElement,
  context: TapFederatedSurfaceMountContext,
): TapFederatedSurfaceMount {
  // Render into container and attach listeners here.
  return {
    unmount() {
      // Remove listeners, subscriptions, timers, and rendered content here.
    },
  };
}

The generated document bounds html, body, and #tap-root to the webview and disables body scrolling. A full-page miniapp must render one internal vertical scroll owner. React surfaces should use SurfaceViewport, which also documents flex, grid, and nested horizontal overflow rules.

Mount Context

The context contains read-only identity and scope fields:

  • package, namespace, release, installation, contribution, and instance IDs;
  • the verified host origin and package asset base URL;
  • the optional host-canonical control-plane userId for interactive mounts;
  • optional workspace, channel, and conversation IDs;
  • the declared package event publisher; and
  • the live selected-owner snapshot; and
  • the current host-authority snapshot.

userId is the same identity used to resolve trusted MCP {userId} storage selectors; do not substitute an OAuth/OIDC profile subject. It is absent from disposable activation-preflight mounts. Optional scope fields are absent when the instance policy does not supply that scope. They are immutable for the mounted realm. Read and subscribe to context.owner when behavior depends on the currently selected workspace, channel, or conversation, because per-workspace and singleton realms can change owner without remounting. Validate required fields at mount and render a useful empty or unavailable state instead of guessing an ID.

Selected Owner

The selected owner begins as null until the host projects the exact frame's current owner. Subscribe for changes and release the listener during unmount:

const renderOwner = () => {
  const owner = context.owner.getSnapshot();
  container.dataset.conversationId = owner?.conversationId ?? '';
};

renderOwner();
const unsubscribeOwner = context.owner.subscribe(renderOwner);

Host Authority

A candidate realm can mount before it is allowed to perform host-backed work. Read context.hostAuthority.getSnapshot() and subscribe to changes:

const renderAuthority = () => {
  const ready = context.hostAuthority.getSnapshot();
  container.toggleAttribute('aria-busy', !ready);
};

renderAuthority();
const unsubscribeAuthority = context.hostAuthority.subscribe(renderAuthority);

Do not use a delay as a readiness signal. Disable privileged actions until authority is available, and handle authority being removed while the surface remains visible.

Package Events

Event names and schemas belong in manifest.tap.json. The host scopes delivery to the verified package session.

const unsubscribeRefresh = context.events.subscribe(
  'catalog.refresh-requested',
  async (payload) => {
    if (typeof payload !== 'object' || payload === null) return;
    await refreshCatalog();
  },
);

await context.events.publish('catalog.refreshed', {
  contributionId: context.contributionId,
});

Validate received payloads even when you own both publisher and subscriber. Releases can overlap briefly during activation, and an older compatible release may use a previous payload version.

Package Assets

Use resolvePackageAssetUrl for files declared inside the target graph:

import { resolvePackageAssetUrl } from '@theaiplatform/miniapp-sdk/surface';

const helpUrl = resolvePackageAssetUrl(context, 'assets/help.html');

The helper rejects absolute paths, traversal, encoded separator aliases, query strings, fragments, credentials, and paths outside the target directory.

Cleanup

Make unmount idempotent because cleanup can follow user navigation, release activation, permission changes, or an error boundary. Unmount should:

  1. unsubscribe from package events and host-authority changes;
  2. remove global and DOM event listeners;
  3. abort pending work that your code owns;
  4. stop timers, observers, media, and workers; and
  5. remove rendered content.
let mounted = true;

return {
  unmount() {
    if (!mounted) return;
    mounted = false;
    unsubscribeRefresh();
    unsubscribeAuthority();
    container.replaceChildren();
  },
};

SDK requests already accepted by the host can still settle during cleanup. Ignore their UI result after unmount rather than assuming every request can be cancelled.