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 usingdefineReactAppletElement.
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 anapplet.Applet. - Add an applet entry to
.applets/config.tomlsojust 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:, orworkspace: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 bichatThe 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 asbase.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.proxyfor/rpcand/stream.createLocalSdkAliases({ enabled, sdkDistDir })— point@iota-uz/sdkto a local dist. WhenIOTA_SDK_DISTis set, local aliases are enabled by default.createAppletStylesVirtualModulePlugin({ inputCss, outputCssPath, prependCss })— virtual modulevirtual:applet-stylesthat 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.cssand 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:
- From the consumer repo (
eai/backoriota-sdk), runapplet sdk link --sdk-root ../../applets. - Start development with
applet dev(orjust dev). The runner detectsgo.workdependencies, starts SDK watch where applicable, and restarts the critical server process on dependency changes. - To return to published npm versions, run
applet sdk unlink.
Dev env toggles
Per applet (name uppercased, - replaced with _):
IOTA_APPLET_DEV_<NAME>=1enables dev proxyIOTA_APPLET_VITE_URL_<NAME>=http://localhost:<vitePort>overrides Vite targetIOTA_APPLET_ENTRY_<NAME>=/src/main.tsxoverrides Vite entry moduleIOTA_APPLET_CLIENT_<NAME>=/@vite/clientoverrides 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:standaloneApplets 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 buildNotes:
- The repo does not commit build output by default.
- A scaffolded applet includes
modules/<name>/presentation/assets/dist/.keepsogo: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.tsis 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
POSTto/rpc(global applet RPC ingress). - Auth: uses the normal IOTA session/cookies; handlers receive a request
context.Contextwith 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=bichatIOTA_ENGINE_SOCKET=<engine uds>IOTA_APPLET_SOCKET=<applet uds>
- Runtime SDK subpath:
@iota-uz/sdk/applet-runtimedefineApplet({ fetch })auth.currentUser()engine.call(method, params)kv.get|set|del|mgetdb.get|query|insert|patch|replace|deletejobs.enqueue|schedule|list|cancelsecrets.getfiles.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.enqueuebichat.jobs.schedulebichat.jobs.listbichat.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:
nextRunAtlastRunAtlastStatuslastError
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 keysmaster_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.storebichat.files.getbichat.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 dirThis 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 = falseSlice 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 var | New key in .applets/config.toml |
|---|---|
IOTA_APPLET_ENGINE_BICHAT | applets.bichat.engine.runtime |
IOTA_APPLET_ENGINE_BUN_BIN | applets.bichat.engine.bun_bin |
IOTA_APPLET_ENGINE_BICHAT_KV_BACKEND | applets.bichat.engine.backends.kv |
IOTA_APPLET_ENGINE_REDIS_URL | applets.bichat.engine.redis.url |
IOTA_APPLET_ENGINE_BICHAT_DB_BACKEND | applets.bichat.engine.backends.db |
IOTA_APPLET_ENGINE_BICHAT_JOBS_BACKEND | applets.bichat.engine.backends.jobs |
IOTA_APPLET_ENGINE_BICHAT_FILES_BACKEND | applets.bichat.engine.backends.files |
IOTA_APPLET_ENGINE_FILES_DIR | applets.bichat.engine.files.dir |
APPLET_S3_ACCESS_KEY | applets.bichat.engine.s3.access_key_env |
APPLET_S3_SECRET_KEY | applets.bichat.engine.s3.secret_key_env |
IOTA_APPLET_ENGINE_BICHAT_SECRETS_BACKEND | applets.bichat.engine.backends.secrets |
IOTA_APPLET_ENGINE_SECRETS_MASTER_KEY | applets.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_KEYsecrets=postgres: reads/writesapplet_engine_secrets(encrypted at rest).secrets=env: operates on process-localIOTA_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"withConfig.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): usesBrowserRouterwithbasename=<basePath>so the URL stays in sync.router-mode="memory": usesMemoryRouterand does not modify the parent page URL (useful for PiP/embedded experiences).
The host element reads these attributes:
base-pathshell-moderouter-mode