Skip to Content

Applets

Applets are React (Vite) applications embedded into IOTA SDK via the Go applet runtime. They can be served:

  • Embedded in the authenticated IOTA shell (recommended)
  • Standalone for local development

This guide covers running embedded HMR, typed RPC type generation, and runtime behavior.

Mental model

An applet is:

  • A Go registration (modules/<name>/applet.go) that declares:
    • basePath (where the applet is mounted, e.g. /bichat)
    • asset serving (prod: embedded dist/ + Vite manifest, dev: proxy to Vite)
    • shell mode (embedded vs standalone)
    • optional RPC methods (typed router)
  • A Vite + React project (modules/<name>/presentation/web) that mounts via a custom element using defineReactAppletElement.

At runtime, IOTA injects InitialContext into window[<WindowGlobal>]. The frontend reads it via @iota-uz/sdk applet-core utilities.

Create an applet

There is currently no scaffold generator. To create an applet:

  • Create a module under modules/<name>/ and register an applet.Applet.
  • Add an applet entry to .applets/config.toml so just dev <name> can run embedded HMR.

Slice-2 local tooling packages are now available in the applets repo:

  • packages/create-applet (create-applet <name>) for minimal scaffold bootstrap.
  • packages/applet-dev (applet-dev) for local JSON-RPC simulation + applet HTTP proxy.

SDK dependency policy

Applet web packages must pin @iota-uz/sdk to an exact npm version (for example 0.4.20).

  • Do not commit local overrides like file:, link:, or workspace: for @iota-uz/sdk.
  • Local SDK hot reload is provided by just dev <name> via Vite dev aliasing and SDK watch builds.
  • CI enforces this policy with just applet deps-check.

Embedded HMR (one command)

Run the Go server and Vite dev server (HMR) with same-origin proxying through Go:

just dev <name>

Example:

just dev bichat

The command:

  • Reads .applets/config.toml
  • Starts Vite in embedded mode with APPLET_ASSETS_BASE=<basePath>/assets/
  • Enables the Go-side dev proxy via env (IOTA_APPLET_DEV_<NAME>=1)
  • Starts the Go server via air

Then open:

http://localhost:3200<basePath>

Vite contract (base path and env)

The Go dev runner sets these env vars for each applet’s Vite process:

  • APPLET_ASSETS_BASE — must end with /; Vite uses it as base.
  • APPLET_VITE_PORT — Vite dev server port.

Use @iota-uz/sdk/applet/vite helpers so you don’t replicate this by hand:

  • createAppletViteConfig({ basePath, backendUrl, extend }) — full config with base, port, dedupe, and optional proxy/aliases. When using extend, alias and plugin arrays are concatenated with the base; other fields (e.g. server.port) are overridden by your extend object.
  • createAppletBackendProxy({ basePath, backendUrl })server.proxy for /rpc and /stream.
  • createLocalSdkAliases({ enabled, sdkDistDir }) — point @iota-uz/sdk to a local dist. When IOTA_SDK_DIST is set, local aliases are enabled by default.
  • createAppletStylesVirtualModulePlugin({ inputCss, outputCssPath, prependCss }) — virtual module virtual:applet-styles that compiles CSS (via Tailwind CLI when available) or reads a prebuilt file. Defaults: inputCss: "src/index.css", outputCssPath: "dist/style.css".
  • createBichatStylesPlugin() — convenience for BiChat applets: prepends @iota-uz/sdk/bichat/styles.css and uses default paths. See BiChat checklist.

See BiChat Downstream applet checklist for Tailwind and Shadow DOM.

Local SDK iteration in downstream apps

To iterate on @iota-uz/sdk inside a downstream app (e.g. EAI) without publishing:

  1. From the consumer repo (eai/back or iota-sdk), run applet sdk link --sdk-root ../../applets.
  2. Start development with applet dev (or just dev). The runner detects go.work dependencies, starts SDK watch where applicable, and restarts the critical server process on dependency changes.
  3. To return to published npm versions, run applet sdk unlink.

Dev env toggles

Per applet (name uppercased, - replaced with _):

  • IOTA_APPLET_DEV_<NAME>=1 enables dev proxy
  • IOTA_APPLET_VITE_URL_<NAME>=http://localhost:<vitePort> overrides Vite target
  • IOTA_APPLET_ENTRY_<NAME>=/src/main.tsx overrides Vite entry module
  • IOTA_APPLET_CLIENT_<NAME>=/@vite/client overrides Vite client module

The applet dev command sets these automatically.

How the dev proxy works (same-origin HMR)

When dev proxy is enabled:

  • Browser requests GET <basePath>/assets/...
  • The Go server reverse-proxies those requests to Vite (including HMR websocket upgrades)
  • Your app stays same-origin with IOTA (cookies/session work without CORS setup)

Standalone dev (Vite only)

Run the Vite app directly:

pnpm -C modules/<name>/presentation/web run dev:standalone

Applets can inject a mock IOTA context in dev (see src/dev/mockIotaContext.ts).

Build embedded assets (production)

To produce an embedded build (into modules/<name>/presentation/assets/dist):

pnpm -C modules/<name>/presentation/web run build

Notes:

  • The repo does not commit build output by default.
  • A scaffolded applet includes modules/<name>/presentation/assets/dist/.keep so go:embed dist/* compiles on a fresh checkout.

Typed RPC (Go → TypeScript)

Define procedures in Go

Add procedures in modules/<name>/rpc/router.go using applet.TypedRPCRouter:

  • Params and result are Go structs (or primitives)
  • The runtime enforces permissions declared per procedure
  • Procedures are registered via applet.AddProcedure(...)

Generate TypeScript types

Generate rpc.generated.ts for an applet:

just applet rpc-gen <name>

This writes:

  • ui/src/<name>/data/rpc.generated.ts (canonical SDK contract)
  • For BiChat, modules/bichat/presentation/web/src/rpc.generated.ts is a re-export shim to avoid duplicate contracts.

Validate generated contracts are in sync:

just applet rpc-check <name>

Call from the frontend

In the applet web code:

import { createAppletRPCClient } from '@iota-uz/sdk' import type { MyAppletRPC } from './rpc.generated' const rpc = createAppletRPCClient({ endpoint: ctx.config.rpcUIEndpoint! }) await rpc.callTyped<MyAppletRPC>('my.method', { /* params */ })

Security model (RPC)

  • Transport: plain HTTP POST to /rpc (global applet RPC ingress).
  • Auth: uses the normal IOTA session/cookies; handlers receive a request context.Context with tenant/user.
  • Authorization: per-method permission enforcement (RequirePermissions).
  • Data access: applets do not access the database directly; Go handlers do.

Devtools overlay (optional)

Enable devtools via:

  • Query param: ?appletDebug=1, or
  • localStorage.iotaAppletDevtools = "1"

The overlay shows:

  • Resolved runtime config (basePath/assets/rpc endpoint)
  • Current route context
  • Recent RPC calls (start/success/error)

Bun runtime pilot (BiChat)

Slice 1 includes non-user-facing Bun runtime plumbing for BiChat.

  • Configure in .applets/config.toml:
version = 2 [applets.bichat] base_path = "/bi-chat" hosts = ["chat.example.com"] # optional host-based mount [applets.bichat.engine] runtime = "bun" [applets.bichat.frontend] type = "static" # static|ssr [applets.bichat.engine.backends] kv = "memory" db = "memory" jobs = "memory" files = "local" secrets = "env"
  • Runtime env vars passed to Bun:
    • IOTA_APPLET_ID=bichat
    • IOTA_ENGINE_SOCKET=<engine uds>
    • IOTA_APPLET_SOCKET=<applet uds>
  • Runtime SDK subpath: @iota-uz/sdk/applet-runtime
    • defineApplet({ fetch })
    • auth.currentUser()
    • engine.call(method, params)
    • kv.get|set|del|mget
    • db.get|query|insert|patch|replace|delete
    • jobs.enqueue|schedule|list|cancel
    • secrets.get
    • files.store|get|delete

In slice 1, kv/db are in-memory server-only stubs used for runtime validation.

Slice 2 kickoff: Redis KV backend (BiChat)

An optional Redis backend is available for BiChat server-only kv.* methods.

  • .applets/config.toml:
[applets.bichat.engine.backends] kv = "redis" [applets.bichat.engine.redis] url = "redis://localhost:6379"

If Redis backend is enabled without applets.bichat.engine.redis.url, app startup fails fast.

Slice 2 kickoff: Postgres DB backend (BiChat)

An optional PostgreSQL backend is available for BiChat server-only db.* methods.

  • .applets/config.toml:
[applets.bichat.engine.backends] db = "postgres"

This backend stores applet records in applet_engine_documents (see migrations). If enabled without an initialized application DB pool, app startup fails fast. If modules/<applet>/runtime/schema.artifact.json exists, startup validates persisted documents against required fields and fails fast on violations. Generate the artifact from runtime/schema.ts with:

applet schema export --name <applet>

Slice 2 kickoff: Jobs API + Postgres backend (BiChat)

Server-only jobs methods are available in BiChat namespace:

  • bichat.jobs.enqueue
  • bichat.jobs.schedule
  • bichat.jobs.list
  • bichat.jobs.cancel

Optional Postgres persistence:

  • .applets/config.toml:
[applets.bichat.engine.backends] jobs = "postgres"

This backend stores jobs in applet_engine_jobs (see migrations) and automatically enables the background poller that dispatches queued jobs and due scheduled jobs to Bun (/__job). If enabled without an initialized application DB pool, app startup fails fast.

Scheduled jobs now include runtime metadata in jobs.schedule/jobs.list responses:

  • nextRunAt
  • lastRunAt
  • lastStatus
  • lastError

Slice 2 kickoff: Secrets API (BiChat)

Server-only method is available in BiChat namespace:

  • bichat.secrets.get

Environment-backed secret contract:

  • IOTA_APPLET_SECRET_<APPLET>_<NAME>
  • Example: IOTA_APPLET_SECRET_BICHAT_OPENAI_API_KEY=...

Optional PostgreSQL backend (encrypted at rest):

  • .applets/config.toml:
[applets.bichat.engine.backends] secrets = "postgres" [applets.bichat.engine.secrets] master_key_file = "/run/secrets/applet_engine_master_key" required = ["OPENAI_API_KEY"] # optional startup-required keys

master_key_file must point to a file containing a base64-encoded 32-byte key.

With this backend, bichat.secrets.get reads encrypted values from applet_engine_secrets.

Slice 2 kickoff: Files API (BiChat)

Server-only methods are available in BiChat namespace:

  • bichat.files.store
  • bichat.files.get
  • bichat.files.delete

Current storage backend is local disk with tenant+applet scoping under the engine temp directory. Runtime SDK now uses engine-socket binary endpoints (/files/store, /files/get, /files/delete) by default, with JSON-RPC fallback for compatibility.

Optional Postgres metadata backend:

  • .applets/config.toml:
[applets.bichat.engine.backends] files = "postgres" [applets.bichat.engine.files] dir = "/var/lib/iota/applet-files" # optional; defaults to temp dir

This backend persists metadata in applet_engine_files and stores file bytes on local disk.

Optional S3 backend (metadata in Postgres, bytes in S3):

[applets.bichat.engine.backends] files = "s3" [applets.bichat.engine.s3] bucket = "iota-applets" region = "us-east-1" endpoint = "https://s3.amazonaws.com" # optional for S3-compatible providers access_key_env = "APPLET_S3_ACCESS_KEY" secret_key_env = "APPLET_S3_SECRET_KEY" force_path_style = false

Slice 2 kickoff: WebSocket bridge (BiChat)

Browser WebSocket endpoint:

  • GET /applets/bichat/ws

Server-only RPC method:

  • bichat.ws.send

Go forwards browser messages to Bun via POST /__ws and Bun runtime SDK exposes:

  • ws.onConnection(handler)
  • ws.onMessage(handler)
  • ws.onClose(handler)
  • ws.send(connectionId, data)

Migration table (env -> config)

Removed env varNew key in .applets/config.toml
IOTA_APPLET_ENGINE_BICHATapplets.bichat.engine.runtime
IOTA_APPLET_ENGINE_BUN_BINapplets.bichat.engine.bun_bin
IOTA_APPLET_ENGINE_BICHAT_KV_BACKENDapplets.bichat.engine.backends.kv
IOTA_APPLET_ENGINE_REDIS_URLapplets.bichat.engine.redis.url
IOTA_APPLET_ENGINE_BICHAT_DB_BACKENDapplets.bichat.engine.backends.db
IOTA_APPLET_ENGINE_BICHAT_JOBS_BACKENDapplets.bichat.engine.backends.jobs
IOTA_APPLET_ENGINE_BICHAT_FILES_BACKENDapplets.bichat.engine.backends.files
IOTA_APPLET_ENGINE_FILES_DIRapplets.bichat.engine.files.dir
APPLET_S3_ACCESS_KEYapplets.bichat.engine.s3.access_key_env
APPLET_S3_SECRET_KEYapplets.bichat.engine.s3.secret_key_env
IOTA_APPLET_ENGINE_BICHAT_SECRETS_BACKENDapplets.bichat.engine.backends.secrets
IOTA_APPLET_ENGINE_SECRETS_MASTER_KEYapplets.bichat.engine.secrets.master_key_file

Secrets CLI (CLI-first)

Manage applet secrets from the applet CLI:

applet secrets set --name bichat --key OPENAI_API_KEY --value "..." applet secrets list --name bichat applet secrets delete --name bichat --key OPENAI_API_KEY
  • secrets=postgres: reads/writes applet_engine_secrets (encrypted at rest).
  • secrets=env: operates on process-local IOTA_APPLET_SECRET_<APPLET>_<NAME> env vars.

Translations

The backend injects translations into ctx.locale.translations (consumed via useTranslation()).

By default, the runtime includes all translations for the current locale. If payload size becomes a concern, you can scope translations per applet:

  • Config.I18n.Mode = "prefixes" with Config.I18n.Prefixes = []string{"MyApplet.", "Common."}
  • Config.I18n.Mode = "none" if the applet does not need backend-provided translations

Router mode (embedded vs standalone)

Applets can run with either:

  • router-mode="url" (default): uses BrowserRouter with basename=<basePath> so the URL stays in sync.
  • router-mode="memory": uses MemoryRouter and does not modify the parent page URL (useful for PiP/embedded experiences).

The host element reads these attributes:

  • base-path
  • shell-mode
  • router-mode
Last updated on