Files
tatuchat/docs/miniapps.md
Welton Moura 6c689d1f34
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
feat: tela de apps com logos/menu de exclusao, correcao de tema nas mini apps e sidebar de navegacao
2026-09-19 00:08:35 -03:00

7.8 KiB
Raw Blame History

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 <meta> tags of the document and uses them as the app's manifest. Every tag listed below must be present in the <head> 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.

<meta name="mini-app-id" content="myapp">
<meta name="mini-app-name" content="My App">
<meta name="mini-app-category" content="productivity">
<meta name="mini-app-description" content="What my app does">
<meta name="mini-app-logo" content="">                 <!-- empty is allowed -->
<meta name="mini-app-in-chat" content="false">          <!-- true|false -->

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.<id> and its HTML source under im.tatuchat.miniapps.source.<id>. 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:

{
  api: 'fromMiniApp',
  widgetId: '<appId>',
  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):

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.<appId>). Resolves with a plain object.

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.

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:

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 <img src> – the homeserver usually requires an authenticated media request that a plain <img> 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.

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.