# TatuChat Mini Apps Mini apps are small, self-contained HTML documents that run inside TatuChat in a sandboxed web view. They can read/write their own key-value store, inspect the user's profile and read/write arbitrary account data through a tiny messaging bridge. There are no dependencies and no build step: an app is just one `.html` file. ## Creating a mini app 1. Write a single `.html` file (assets may be inlined; see limitations below). 2. Import it in TatuChat: - **From file:** open *Apps* → the **import** menu → *Import from file* and pick your `.html` file. - **From URL:** *Apps* → import menu → *Import from URL* and paste a link to the HTML document. 3. Open the app from the *Installed* list. Use the reload button in the top bar while developing. The available apps page (`/rooms/apps`) lists the built-in apps (`tasks`, `accountdata`) under *Installed* once you install them, plus every app you import. ## Manifest (meta tags) Before installing an app, TatuChat reads the `` tags of the document and uses them as the app's manifest. **Every tag listed below must be present** in the `` of your document, even if the value is empty — except `id` and `name`, which must also be non-empty. An app that is missing any of these tags **cannot be installed**. ```html ``` `mini-app-in-chat` must be `true` or `false` (empty defaults to `false`). Values are trimmed and read case-insensitively. The `mini-app-id` is used **as the key in the account data store**: the app's store is persisted under `im.tatuchat.miniapps.data.` and its HTML source under `im.tatuchat.miniapps.source.`. It must be unique: ids of built-in apps (`tasks`, `accountdata`) are reserved and an already installed id cannot be reused. This id is also the `widgetId` your app must send in the bridge protocol. The remaining fields are stored as metadata (`category`, `description`, `logo`, `inChat`) and shown in the *Apps* page. ## Talking to TatuChat The host listens for `message` events. A channel app sends a request with `window.parent.postMessage`, and the host answers with the same `requestId`. Replies are delivered in an event with a `response` object of the form `{data, error}`. Request payload: ```js { api: 'fromMiniApp', widgetId: '', requestId: 'any-unique-string', action: 'io.tatuchat.storage.load', // one of the actions below data: { /* action-specific payload */ } } ``` Helper for your app (copy this into your HTML): ```js const APP_ID = 'myapp'; // must match the app id const _pending = new Map(); let _requestCounter = 0; function bridgeRequest(action, data) { return new Promise(function (resolve, reject) { const requestId = 'r' + (++_requestCounter); _pending.set(requestId, { resolve: resolve, reject: reject }); window.parent.postMessage({ api: 'fromMiniApp', widgetId: APP_ID, requestId: requestId, action: action, data: data }, '*'); }); } window.addEventListener('message', function (event) { const msg = event.data; if (!msg || msg.api !== 'fromMiniApp' || !msg.requestId) return; if (msg.response === undefined) return; if (!_pending.has(msg.requestId)) return; const pending = _pending.get(msg.requestId); _pending.delete(msg.requestId); const response = msg.response; if (response.error) pending.reject(new Error(response.error)); else pending.resolve(response.data); }); ``` ## Available actions ### `io.tatuchat.storage.load` Loads the app's own key-value store (persisted in the user's account data under `im.tatuchat.miniapps.data.`). Resolves with a plain object. ```js const data = await bridgeRequest('io.tatuchat.storage.load'); // data => { ... } (any JSON values previously saved) ``` ### `io.tatuchat.storage.save` Replaces the whole store. `data` must be an object. Resolves with `null`. ```js await bridgeRequest('io.tatuchat.storage.save', { tasks: [...], version: 1 }); ``` > Note: this **replaces** the entire store; there is no per-key merge. Load, > mutate, then save. ### `io.tatuchat.identity.get` Resolves with the signed-in user's profile: ```js const info = await bridgeRequest('io.tatuchat.identity.get'); // => { // userId: '@user:server', // displayName: 'Tatu', // avatarUrl: 'mxc://server/...', // avatarHttpUrl: 'https://.../download/...', // may require auth // avatarDataUri: 'data:image/png;base64,...', // always renderable // homeserver: 'https://server.example' // } ``` The `avatarDataUri` is a base64 data URI fetched by TatuChat with the user's credentials. **Use `avatarDataUri` for ``** – the homeserver usually requires an authenticated media request that a plain `` tag cannot send (this is what caused the `M_MISSING_TOKEN` error). Fall back to `avatarHttpUrl` only if `avatarDataUri` is absent. ### `io.tatuchat.theme.get` Resolves with the color palette currently used by TatuChat, so your app can match the ecosystem (light/dark aware). Colors are `#RRGGBBAA` hex strings (CSS 8-digit order: red, green, blue, alpha) suitable for CSS. ```js const { palette } = await bridgeRequest('io.tatuchat.theme.get'); // => { // brightness: 'light' | 'dark', // primary: '#00D084FF', // onPrimary: '#000000FF', // primaryContainer: '#00D08433', // surface: '#FEF7FF00', // surfaceContainerLowest/Low/Container/High/Highest: '#…', // secondary..., tertiary..., error..., // outline, outlineVariant, background, onBackground, onSurface, ... // } document.documentElement.style.setProperty('--primary', palette.primary); ``` ### `io.tatuchat.accountdata.list` Resolves with a map of **all** account data of the signed-in user as an object of `{ key: { ...content } }`. ### `io.tatuchat.accountdata.get` `data: { key: 'org.example.something' }` resolves with `{ key, content }` where `content` is the stored JSON content (or `{}` if absent). ### `io.tatuchat.accountdata.set` `data: { key: 'org.example.something', content: { ... } }` stores or replaces the content of that account data key. Resolves with `null`. ## Caveats & limitations - **Distribution / installation:** apps imported from URL must be served with permissive CORS headers (`Access-Control-Allow-Origin`) so the browser can read them. On the web, TatuChat retries through a CORS proxy if you configure one (setting `miniAppCorsProxy` in the app settings/config). If the server blocks cross-origin reads, import the `.html` file manually instead. - **Size:** the HTML source is stored in the user's account data, where individual events are limited to about **64 KB** (homeservers like matrix.org enforce this). Keep mini apps small and inline the assets; or load heavy content from a URL at runtime. - **Security:** the app runs in a sandboxed web view with `allow-scripts` / `allow-forms` / `allow-same-origin`. It can only reach TatuChat through the bridge actions above – it cannot access the user's rooms, messages or files. - **No unsolicited access:** data is only exchanged when the app explicitly calls the bridge. The host never pushes data into the app on its own. - **Store is per app:** every app has its own `storage.load`/`save` namespace. `accountdata.*` is the only way to share data between apps. - **`storage.save` overwrites:** there is no merge/patch semantics. - **Native platforms:** the HTML is rendered by the platform web view; take care with features that differ between browsers (e.g. storage APIs inside the sandbox, `window.parent` assumptions). Prefer the bridge over `localStorage`.