Quickstart

This guide builds a small desktop miniapp from an empty directory. It uses only packages available from the public npm registry.

Prerequisites

  • Node.js 20 or later
  • pnpm 10.33 or later
  • TypeScript 5.9 or later
  • A supported installation of The AI Platform for loading the result

Create the Project

mkdir hello-miniapp
cd hello-miniapp
pnpm init
pnpm add @theaiplatform/miniapp-sdk
pnpm add -D @module-federation/rsbuild-plugin@2.8.0 @module-federation/runtime-tools@2.8.0 @rsbuild/core@^2 @rslib/core@^0.23 typescript

Set the project to ESM and add build scripts:

package.json
{
  "name": "hello-miniapp",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "pnpm build:target && node scripts/assemble.mjs",
    "build:target": "rslib build"
  }
}

Declare the Package

Create manifest.tap.json. Identifiers are permanent package identity, so replace the example publisher and organization values with identifiers assigned to you.

manifest.tap.json
{
  "$schema": "./node_modules/@theaiplatform/miniapp-sdk/config-schema.json",
  "descriptorVersion": 1,
  "package": {
    "packageId": "tap_pkg_example_hello_0001",
    "publisherId": "publisher_example",
    "organizationId": "organization_example",
    "namespace": "example-hello",
    "slug": "hello"
  },
  "release": {
    "releaseId": "tap_pkg_example_hello_0001@0.1.0",
    "version": "0.1.0",
    "contentDigest": "pending"
  },
  "presentation": {
    "name": "Hello Miniapp",
    "description": "A minimal desktop miniapp built with the public SDK."
  },
  "compatibility": {
    "tapSdk": ">=0.1.0 <1",
    "tapHost": ">=0.1.0"
  },
  "targets": {
    "desktop": {
      "remoteName": "example_hello_desktop",
      "remoteEntry": "remoteEntry.mjs",
      "remoteEntryIntegrity": "pending",
      "manifest": "mf-manifest.json",
      "manifestIntegrity": "pending",
      "assetLock": "tap.package.lock.json",
      "assetLockIntegrity": "pending",
      "libraryType": "module",
      "exposes": {
        "./ui/desktop": {
          "integrity": "pending",
          "runtime": "webview"
        }
      }
    }
  },
  "contributions": [
    {
      "kind": "ui.surface",
      "id": "hello-surface",
      "apiVersion": 1,
      "targets": {
        "desktop": {
          "expose": "./ui/desktop",
          "runtime": "webview"
        }
      },
      "lifecycleScope": "mount",
      "authorization": {
        "allOf": ["channels.list"]
      },
      "options": {
        "displayName": "Hello Miniapp",
        "description": "List the channels visible to this miniapp.",
        "placement": "workspace-left",
        "scope": "workspace",
        "instancePolicy": "per-workspace",
        "persistence": "none"
      }
    },
    {
      "kind": "miniapp",
      "id": "hello",
      "apiVersion": 1,
      "options": {
        "contributionIds": ["hello-surface"]
      }
    },
    {
      "kind": "permission.catalog",
      "id": "hello-permissions",
      "apiVersion": 1,
      "options": {
        "actions": [
          {
            "id": "channels.list",
            "resource": "tap.channels:list",
            "scopes": ["workspace"],
            "directActors": ["human"],
            "delegatedActors": [],
            "autonomyCeiling": "listen",
            "consent": "none",
            "risk": "read"
          }
        ],
        "levels": [
          {
            "id": "channel-reader",
            "actions": ["channels.list"],
            "assignableTo": ["human"]
          }
        ],
        "roleRecommendations": [
          {
            "actor": "human",
            "workspaceRole": "member",
            "level": "channel-reader"
          }
        ]
      }
    }
  ]
}

pending fields are build inputs, not values that a host trusts. The SDK build replaces them with the emitted graph's integrity and content digest.

The permission catalog declares the channels.list action used by the example, and the surface binds that action through authorization.allOf. These declarations request a grant; they do not create one. During installation, the user or workspace administrator must review and approve the requested channel-reading level before the button can list channels. A denied or later-revoked grant makes the SDK call reject, so the surface keeps its error handling.

Implement the Surface

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

export function mount(
  container: HTMLElement,
  context: TapFederatedSurfaceMountContext,
): TapFederatedSurfaceMount {
  const heading = document.createElement('h1');
  heading.textContent = 'Hello from a verified miniapp';

  const button = document.createElement('button');
  button.textContent = 'List channels';

  const output = document.createElement('pre');
  button.addEventListener('click', async () => {
    try {
      const result = await sdk.channels.list({
        workspaceId: context.workspaceId,
      });
      output.textContent = JSON.stringify(result.rooms, null, 2);
    } catch (error) {
      output.textContent =
        error instanceof Error ? error.message : 'The request failed.';
    }
  });

  container.replaceChildren(heading, button, output);

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

The host installs the SDK capability session before it evaluates this module. Importing sdk is safe in any JavaScript environment, but reading one of its properties before the host session exists throws an unsupported-environment error.

Configure the Target Build

rslib.config.ts
import { defineConfig } from '@rslib/core';
import { tapLib } from '@theaiplatform/miniapp-sdk/rspack';

const desktop = tapLib({
  manifest: './manifest.tap.json',
  packageTarget: 'desktop',
  packageOutputRoot: '.tap-build/desktop',
  federation: {
    name: 'example_hello_desktop',
    filename: 'remoteEntry.mjs',
    manifest: true,
    library: { type: 'module' },
    dts: false,
    exposes: {
      './ui/desktop': './src/surface.ts',
    },
  },
});

export default defineConfig({ lib: [desktop] });

Assemble the independently built target into the publication directory:

scripts/assemble.mjs
import {
  assembleTapPackage,
  assertPortableTapPackageArtifacts,
} from '@theaiplatform/miniapp-sdk/rspack';

await assembleTapPackage({
  manifest: './manifest.tap.json',
  output: './dist',
  targets: {
    desktop: './.tap-build/desktop',
  },
});

await assertPortableTapPackageArtifacts({
  output: './dist',
});

Build

pnpm build

The complete package is now in dist. Load its manifest.tap.json through the Miniapps settings in The AI Platform, or continue to Deploy and Live Update.