@pstdio/tiny-ui 
Browser-first plugin runtime for sandboxed micro frontends.
Compile OPFS-backed sources with esbuild-wasm, cache bundles in a service worker, and expose audited host capabilities to plugin iframes.
Install 
npm i @pstdio/tiny-uiWhy Tiny UI? 
- Build and ship third-party plugin UIs entirely in the browser—no server build step required.
- Publish compiled bundles to the Cache API and serve them through a dedicated service worker + runtime iframe.
- Hand plugins a typed hostbridge through a singleonActionCallRPC surface.
- Reuse the Tiny Plugins lockfile/import-map tooling so bare specifiers resolve deterministically.
Quick Start 
1. Serve the runtime assets 
Expose the runtime HTML and service worker from your app origin. With Vite (or any bundler that supports the ?url suffix), you can import the asset URLs directly:
// host/bootstrap.ts
import runtimeUrl from "@pstdio/tiny-ui/dist/runtime.html?url";
import serviceWorkerUrl from "@pstdio/tiny-ui-bundler/dist/sw.js?url";
import { setupTinyUI } from "@pstdio/tiny-ui";
void setupTinyUI({ runtimeUrl, serviceWorkerUrl }).catch(console.error);If your bundler cannot import assets as URLs, copy @pstdio/tiny-ui/dist/runtime.html and @pstdio/tiny-ui-bundler/dist/sw.js to /tiny-ui/runtime.html and /tiny-ui-sw.js, then call setupTinyUI({ runtimeUrl: "/tiny-ui/runtime.html", serviceWorkerUrl: "/tiny-ui-sw.js" }) during your app bootstrap.
2. Register a virtual project snapshot 
Load the plugin source tree (for example, from OPFS) and cache it with Tiny UI. The compile step reads this snapshot when it builds the bundle.
import { compile, loadSnapshot, setLockfile } from "@pstdio/tiny-ui";
import { registerSources } from "@pstdio/tiny-ui-bundler";
setLockfile({
  react: "https://esm.sh/react@19.1.0/es2022/react.mjs",
  "react-dom/client": "https://esm.sh/react-dom@19.1.0/es2022/client.mjs",
});
const source = {
  id: "weather-ui",
  root: "/plugins/weather-ui",
  entrypoint: "/index.tsx",
};
await loadSnapshot(source.root, source.entrypoint);
registerSources([{ id: source.id, root: source.root, entry: source.entrypoint }]);
const result = await compile(source.id, {
  wasmURL: "https://unpkg.com/esbuild-wasm@0.25.10/esbuild.wasm",
});
console.log(result.hash, result.url, result.assets);3. Wrap React trees with TinyUiProvider 
Use TinyUiProvider instead of calling setupTinyUI directly when you are rendering the React wrapper. The provider bootstraps the runtime and service worker for you and exposes the compile helper via useTinyUi().
import { TinyUiProvider, TinyUI } from "@pstdio/tiny-ui";
import { registerSources } from "@pstdio/tiny-ui-bundler";
import runtimeUrl from "@pstdio/tiny-ui/dist/runtime.html?url";
import serviceWorkerUrl from "@pstdio/tiny-ui-bundler/dist/sw.js?url";
const hostApi = {
  "actions.log": (params?: Record<string, unknown>) => {
    console.log("[weather-ui]", params?.message ?? "<no message>");
    return { ok: true };
  },
};
registerSources([{ id: "weather-ui", root: "/plugins/weather-ui" }]);
function PluginFrame() {
  return (
    <TinyUiProvider runtimeUrl={runtimeUrl} serviceWorkerUrl={serviceWorkerUrl}>
      <TinyUI
        instanceId="weather-ui-runtime"
        sourceId="weather-ui"
        autoCompile
        onStatusChange={(status) => console.log("Tiny UI status", status)}
        onError={(error) => console.error(error)}
        onActionCall={(method, params) => {
          const handler = hostApi[method as keyof typeof hostApi];
          if (!handler) {
            throw new Error(`Unhandled Tiny UI host method: ${method}`);
          }
          return handler(params as Record<string, unknown> | undefined);
        }}
      />
    </TinyUiProvider>
  );
}- TinyUiProvideraccepts- serviceWorkerUrl,- runtimeUrl, and optional overrides like- wasmURLif you self-host the- esbuild-wasmbinary.
- instanceIduniquely identifies the iframe host session.
- sourceIdmust match the ID you registered via- registerSourceswhen seeding the snapshot.
- onActionCallis the single entrypoint for routing- remote.opsrequests to your application API.
- Call useTinyUi()inside descendant components to access the sharedcompilehelper and service worker state, oruseTinyUIServiceWorker()when you only need status information.
4. Boot a raw iframe 
Use the lower-level host APIs when you are not rendering the React wrapper. Wire an iframe straight to the Tiny UI runtime and call setupTinyUI yourself during application bootstrap.
<button id="load-plugin">Load plugin</button>
<iframe id="tiny-ui-iframe" title="tiny-ui" style="width: 100%; height: 420px; border: 1px solid #ccc;"></iframe>
<script type="module">
  import { compile, createTinyHost, loadSnapshot, setLockfile } from "@pstdio/tiny-ui";
  import { registerSources } from "@pstdio/tiny-ui-bundler";
  const iframe = document.getElementById("tiny-ui-iframe");
  const button = document.getElementById("load-plugin");
  const source = {
    id: "weather-ui",
    root: "/plugins/weather-ui",
    entrypoint: "/index.tsx",
  };
  setLockfile({
    react: "https://esm.sh/react@19.1.0/es2022/react.mjs",
    "react-dom/client": "https://esm.sh/react-dom@19.1.0/es2022/client.mjs",
  });
  const hostApi = {
    "actions.log": async (params) => {
      console.log("[weather-ui] log", params?.message ?? "<no message>");
      return { ok: true };
    },
  };
  button.addEventListener("click", async () => {
    await loadSnapshot(source.root, source.entrypoint);
    registerSources([{ id: source.id, root: source.root, entry: source.entrypoint }]);
    const host = await createTinyHost(iframe, source.id, {
      onOps: async ({ method, params }) => {
        const handler = hostApi[method];
        if (!handler) {
          throw new Error(`Unknown Tiny UI host method: ${method}`);
        }
        return handler(params);
      },
      onReady: ({ meta }) => console.log("Plugin ready", meta),
      onError: ({ message }) => console.error("Plugin failed", message),
    });
    const compileResult = await compile(source.id, {
      wasmURL: "https://unpkg.com/esbuild-wasm@0.25.10/esbuild.wasm",
    });
    await host.sendInit(compileResult);
  });
</script>5. Handle remote.ops requests 
Plugins invoke remote.ops (for example through host.actions.*) whenever they need something from the host. TinyUI surfaces each of those calls through onActionCall(method, params). Route the call to your own application API, return a result (or throw to reject), and you're done.
API Reference 
Bootstrap 
- setupTinyUI(options)– configure Tiny UI once per page (registers the service worker, sets the runtime URL, and primes global state). Use this when you integrate with the vanilla host APIs.
- setupServiceWorker(options)– lower-level helper to register only the service worker.
- getTinyUIRuntimePath()– current runtime iframe URL resolved from- setupTinyUI.
- TinyUiProvider(props)– React context provider that wraps your tree, calls- setupTinyUIinternally, and exposes a memoised- compilehelper. Accepts- serviceWorkerUrl,- runtimeUrl, and optional overrides like- wasmURL.
Core Components & Hooks 
- TinyUI(props)– React component that compiles snapshots and boots the runtime iframe. Accepts lifecycle callbacks,- autoCompile, and an- onActionCallhandler for host RPCs.
- TinyUIStatus– status union (- "idle" | "initializing" | "compiling" | "handshaking" | "ready" | "error").
- useTinyUi()– access the provider context (- compile,- status,- serviceWorkerReady,- error).
- useTinyUIServiceWorker()– React hook that exposes the shared service worker lifecycle (- status,- serviceWorkerReady, and- error).
Snapshot Management 
- registerVirtualSnapshot(root, snapshot)/- unregisterVirtualSnapshot(root)– cache the in-memory file tree Tiny UI will compile.
- loadSnapshot(root, entry)– read OPFS into a snapshot and register it for compilation.
- loadSourceFiles({ id, root, entrypoint })– OPFS helper that reads a plugin directory and returns file metadata for registration.
Build & Compilation 
- compile(id, options)– compile a registered snapshot using esbuild-wasm and cache the result.
- getCachedBundle(id)– retrieve a previously compiled bundle from cache.
Lockfile & Import Maps 
- setLockfile(lockfile)/- getLockfile()– manage remote module metadata.
- buildImportMap(lockfile)– convert a lockfile into an import map for the runtime iframe.
Low-Level Host Integration 
- createTinyHost(iframe, id, callbacks)– low-level host connector returning- sendInit/- disconnect; wire lifecycle + RPC handlers through- callbacks.
Constants 
- CACHE_NAME,- RUNTIME_HTML_PATH,- VIRTUAL_PREFIX– constants that mirror the service worker config.
Examples 
Load OPFS files once, reuse across reloads 
import { loadSnapshot, TinyUI, CACHE_NAME, setupTinyUI } from "@pstdio/tiny-ui";
import { registerSources } from "@pstdio/tiny-ui-bundler";
async function bootPlugin() {
  void setupTinyUI({ runtimeUrl: "/tiny-ui/runtime.html", serviceWorkerUrl: "/tiny-ui-sw.js" }).catch(
    console.error,
  );
  await loadSnapshot("plugins/notepad", "/index.tsx");
  registerSources([{ id: "notepad", root: "/plugins/notepad" }]);
  render(
    <TinyUI
      instanceId="notepad-host"
      sourceId="notepad"
      onActionCall={(method, params) => {
        console.log("Unhandled request", method, params);
        return { ok: true };
      }}
    />
  );
}
async function invalidateBundles() {
  if (typeof caches === "undefined") return;
  await caches.delete(CACHE_NAME);
}Instant iframe boot from the cache manifest 
If you previously compiled a plugin and the service worker still holds the bundle, you can skip compile entirely and boot straight from the cache manifest.
import { compile, createTinyHost, getCachedBundle, loadSnapshot } from "@pstdio/tiny-ui";
import { registerSources } from "@pstdio/tiny-ui-bundler";
const pluginId = "sql-explorer";
const iframe = document.querySelector("iframe#plugin")!;
const hostApi = {
  "actions.log": (params?: Record<string, unknown>) => {
    console.log("[sql-explorer]", params?.message ?? "<no message>");
    return { ok: true };
  },
};
const host = await createTinyHost(iframe, pluginId, {
  onOps: async ({ method, params }) => {
    const handler = hostApi[method as keyof typeof hostApi];
    if (!handler) throw new Error(`Unhandled Tiny UI host method: ${method}`);
    return handler(params as Record<string, unknown> | undefined);
  },
  onReady: ({ meta }) => console.log("Plugin ready", meta),
  onError: ({ message }) => console.error("Plugin failed", message),
});
let result = await getCachedBundle(pluginId);
if (!result) {
  const source = {
    id: pluginId,
    root: "/plugins/sql-explorer",
    entrypoint: "/index.tsx",
  };
  await loadSnapshot(source.root, source.entrypoint);
  registerSources([{ id: source.id, root: source.root, entry: source.entrypoint }]);
  result = await compile(pluginId, {
    wasmURL: "https://unpkg.com/esbuild-wasm@0.25.10/esbuild.wasm",
  });
}
await host.sendInit(result);Usage Notes 
- The first compile downloads esbuild-wasm(≈1 MB); host environments can pass a customruntimeUrlviasetupTinyUI({ runtimeUrl })or overridewasmURLonTinyUiProvider/compile()to point at a local mirror.
- Serve runtime.htmlandsw.jsfrom the same origin as the iframe; cross-origin service workers are blocked by browsers. React apps should pass these URLs throughTinyUiProviderwhile vanilla hosts callsetupTinyUIdirectly.
- Snapshots must include the designated entry file. Tiny UI throws if the entry is missing so errors surface during development.
- Use setLockfile()to declare CDN-backed dependencies; the runtime injects the import map before loading the bundle. The lockfile is shared across all Tiny UI instances, so the latest call wins—merge specifiers when multiple plugins need different libraries.
- When running in non-OPFS browsers, build hosts can still register virtual snapshots by supplying file contents directly.
Dependencies 
- @pstdio/opfs-utils - Core OPFS operations
- @pstdio/tiny-plugins - Plugin manifest and runtime
- esbuild-wasm- Browser-based bundling
- react- UI framework support
See Also 
- @pstdio/tiny-plugins - Plugin manifest and command execution
- Plugins - Building plugins for Kaset
- Live Playground - Try plugin UIs in action