feat: Add browser documentation

This commit is contained in:
2026-04-29 17:15:01 +02:00
parent d261bac0ed
commit 3d81c34159
29 changed files with 19981 additions and 40 deletions

View File

@@ -0,0 +1,301 @@
---
sidebar_position: 4
---
# Plugin API Reference
`TojuClientPluginApi` is the object passed to a plugin activation context. The runtime freezes the API object before passing it to plugin code.
## Activation Types
```ts
interface TojuPluginDisposable {
dispose: () => void;
}
interface TojuPluginActivationContext {
api: TojuClientPluginApi;
manifest: TojuPluginManifest;
pluginId: string;
subscriptions: TojuPluginDisposable[];
}
interface TojuClientPluginModule {
activate?: (context: TojuPluginActivationContext) => Promise<void> | void;
deactivate?: (context: TojuPluginActivationContext) => Promise<void> | void;
onPluginDataChanged?: (context: TojuPluginActivationContext, event: unknown) => Promise<void> | void;
onServerRequirementsChanged?: (context: TojuPluginActivationContext, snapshot: PluginRequirementsSnapshot) => Promise<void> | void;
ready?: (context: TojuPluginActivationContext) => Promise<void> | void;
}
```
## Profiles
```ts
interface PluginApiProfileUpdate {
description?: string;
displayName: string;
}
interface PluginApiAvatarUpdate {
avatarHash: string;
avatarMime: string;
avatarUrl: string;
}
```
| Method | Capability | Description |
| --- | --- | --- |
| `profile.getCurrent()` | `profile.read` | Returns the current `User` or `null`. |
| `profile.update(profile)` | `profile.write` | Updates display name and optional description. |
| `profile.updateAvatar(avatar)` | `profile.write` | Updates avatar URL, MIME type, and hash metadata. |
## Users and Roles
| Method | Capability | Description |
| --- | --- | --- |
| `users.getCurrent()` | `users.read` | Returns current `User` or `null`. |
| `users.list()` | `users.read` | Returns known users. |
| `users.readMembers()` | `users.read` | Returns active room members. |
| `users.setRole(userId, role)` | `roles.manage` | Updates a user's role. |
| `users.kick(userId)` | `users.manage` | Kicks a user. |
| `users.ban(userId, reason?)` | `users.manage` | Bans a user with optional reason. |
| `roles.list()` | `roles.read` | Returns room roles. |
| `roles.setAssignments(assignments)` | `roles.manage` | Replaces role assignments. |
## Server
```ts
interface PluginApiServerSettingsUpdate {
description?: string;
isPrivate?: boolean;
maxUsers?: number;
name?: string;
password?: string;
topic?: string;
}
interface PluginApiPluginUserRequest {
avatarUrl?: string;
displayName: string;
id?: string;
}
```
| Method | Capability | Description |
| --- | --- | --- |
| `server.getCurrent()` | `server.read` | Returns the current `Room` or `null`. |
| `server.registerPluginUser(request)` | `users.manage` | Adds a plugin-owned user and returns its id. |
| `server.updatePermissions(permissions)` | `server.manage` | Updates partial room permissions. |
| `server.updateSettings(settings)` | `server.manage` | Updates room settings. |
## Channels
```ts
interface PluginApiChannelRequest {
id?: string;
name: string;
position?: number;
}
```
| Method | Capability | Description |
| --- | --- | --- |
| `channels.list()` | `channels.read` | Returns current room channels. |
| `channels.select(channelId)` | `channels.read` | Selects a channel. |
| `channels.addAudioChannel(request)` | `channels.manage` | Adds a voice channel. |
| `channels.addVideoChannel(request)` | `channels.manage` | Registers a video channel section. |
| `channels.rename(channelId, name)` | `channels.manage` | Renames a channel. |
| `channels.remove(channelId)` | `channels.manage` | Removes a channel. |
## Messages
```ts
interface PluginApiMessageAsPluginUserRequest {
channelId?: string;
content: string;
pluginUserId: string;
}
```
| Method | Capability | Description |
| --- | --- | --- |
| `messages.readCurrent()` | `messages.read` | Returns current visible messages. |
| `messages.send(content, channelId?)` | `messages.send` | Sends a message and returns the created `Message`. |
| `messages.sendAsPluginUser(request)` | `messages.send` | Emits a message from a registered plugin user. |
| `messages.edit(messageId, content)` | `messages.editOwn` | Edits a plugin message. |
| `messages.delete(messageId)` | `messages.deleteOwn` | Deletes a plugin message. |
| `messages.moderateDelete(messageId)` | `messages.moderate` | Performs a moderation delete. |
| `messages.sync(messages)` | `messages.sync` | Syncs an array of messages into state. |
## Events
```ts
interface PluginApiEventSubscription {
eventName: string;
handler: (event: PluginEventEnvelope) => void;
}
interface PluginEventEnvelope<TPayload = unknown> {
emittedAt?: number;
eventId?: string;
eventName: string;
payload: TPayload;
pluginId: string;
serverId: string;
sourcePluginUserId?: string;
sourceUserId?: string;
type: 'plugin_event';
}
```
| Method | Capability | Description |
| --- | --- | --- |
| `events.publishServer(eventName, payload)` | `events.server.publish` | Sends a declared plugin event through the signaling server. |
| `events.subscribeServer(subscription)` | `events.server.subscribe` | Subscribes to a declared server plugin event. |
| `events.publishP2p(eventName, payload)` | `events.p2p.publish` | Sends a declared plugin event over peer paths. |
| `events.subscribeP2p(subscription)` | `events.p2p.subscribe` | Registers a P2P event subscription. |
## Message Bus
```ts
interface PluginApiMessageBusEnvelope {
channelId?: string;
eventId: string;
messages?: Message[];
payload?: unknown;
pluginId: string;
roomId: string;
sentAt: number;
sourcePeerId?: string;
sourceUserId?: string;
topic: string;
}
interface PluginApiMessageBusLatestRequest {
channelId?: string;
includeDeleted?: boolean;
limit?: number;
sinceTimestamp?: number;
targetPeerId?: string;
topic?: string;
}
interface PluginApiMessageBusPublishRequest extends PluginApiMessageBusLatestRequest {
includeLatestMessages?: boolean;
includeSelf?: boolean;
payload?: unknown;
topic: string;
}
interface PluginApiMessageBusSubscription {
channelId?: string;
handler: (event: PluginApiMessageBusEnvelope) => void;
latestMessageLimit?: number;
replayLatest?: boolean;
topic?: string;
}
```
| Method | Capability | Description |
| --- | --- | --- |
| `messageBus.publish(request)` | `events.p2p.publish`, optionally `messages.read` | Publishes a plugin-bus event, optionally including latest messages. |
| `messageBus.sendLatestMessages(request?)` | `events.p2p.publish` and `messages.read` | Sends a latest-message snapshot. |
| `messageBus.subscribe(subscription)` | `events.p2p.subscribe`, optionally `messages.read` | Subscribes to plugin-bus events, optionally replaying latest messages. |
## P2P and Media
```ts
interface PluginApiAudioClipRequest {
volume?: number;
url: string;
}
interface PluginApiCustomStreamRequest {
label?: string;
stream: MediaStream;
}
```
| Method | Capability | Description |
| --- | --- | --- |
| `p2p.connectedPeers()` | `p2p.data` | Returns connected peer ids. |
| `p2p.broadcastData(eventName, payload)` | `p2p.data` | Broadcasts plugin data. |
| `p2p.sendData(peerId, eventName, payload)` | `p2p.data` | Sends plugin data targeted to a peer. |
| `media.playAudioClip(request)` | `media.playAudio` | Plays an audio URL at optional volume. |
| `media.addCustomAudioStream(request)` | `media.addAudioStream` | Contributes an audio `MediaStream`. |
| `media.addCustomVideoStream(request)` | `media.addVideoStream` | Registers a video `MediaStream` contribution. |
| `media.setInputVolume(volume)` | `audio.volume` | Sets local input volume. |
| `media.setOutputVolume(volume)` | `audio.volume` | Sets local output volume. |
## Storage
| Method | Capability | Description |
| --- | --- | --- |
| `clientData.read(key)` | `storage.local` | Reads async plugin-local data. |
| `clientData.write(key, value)` | `storage.local` | Writes async plugin-local data. |
| `clientData.remove(key)` | `storage.local` | Removes async plugin-local data. |
| `serverData.read(key)` | `storage.serverData.read` | Reads local per-user/per-server data. |
| `serverData.write(key, value)` | `storage.serverData.write` | Writes local per-user/per-server data. |
| `serverData.remove(key)` | `storage.serverData.write` | Removes local per-user/per-server data. |
| `storage.get(key)` | `storage.local` | Legacy synchronous local read. |
| `storage.set(key, value)` | `storage.local` | Legacy synchronous local write. |
| `storage.remove(key)` | `storage.local` | Legacy synchronous local remove. |
## UI Contributions
```ts
interface PluginApiActionContribution {
icon?: string;
label: string;
run: () => Promise<void> | void;
}
interface PluginApiPageContribution {
label: string;
path: string;
render: () => HTMLElement | string;
}
interface PluginApiPanelContribution {
label: string;
order?: number;
render: () => HTMLElement | string;
}
interface PluginApiSettingsPageContribution {
label: string;
order?: number;
render: () => HTMLElement | string;
settingsKey?: string;
}
interface PluginApiChannelSectionContribution {
label: string;
order?: number;
type?: 'audio' | 'custom' | 'video';
}
interface PluginApiEmbedRendererContribution {
embedType: string;
render: (payload: unknown) => HTMLElement | string;
}
interface PluginApiDomMountRequest {
element: HTMLElement;
position?: InsertPosition;
target: Element | string;
}
```
| Method | Capability | Description |
| --- | --- | --- |
| `ui.registerAppPage(id, contribution)` | `ui.pages` | Adds a plugin app page. |
| `ui.registerSettingsPage(id, contribution)` | `ui.settings` | Adds a plugin settings page. |
| `ui.registerSidePanel(id, contribution)` | `ui.sidePanel` | Adds a side panel. |
| `ui.registerChannelSection(id, contribution)` | `ui.channelsSection` | Adds a channel section. |
| `ui.registerComposerAction(id, contribution)` | `ui.pages` | Adds a composer action. |
| `ui.registerProfileAction(id, contribution)` | `ui.pages` | Adds a profile action. |
| `ui.registerToolbarAction(id, contribution)` | `ui.pages` | Adds a toolbar action. |
| `ui.registerEmbedRenderer(id, contribution)` | `ui.embeds` | Adds an embed renderer. |
| `ui.mountElement(id, request)` | `ui.dom` | Mounts plugin-owned DOM into a target element or selector. |

View File

@@ -0,0 +1,50 @@
---
sidebar_position: 3
---
# Capabilities
Capabilities protect privileged app surfaces. A plugin must declare a capability in its manifest and the user must grant it before the runtime allows the corresponding API call.
| Capability | API areas | Notes |
| --- | --- | --- |
| `profile.read` | `profile.getCurrent()` | Reads the current user. |
| `profile.write` | `profile.update()`, `profile.updateAvatar()` | Updates local profile fields and avatar metadata. |
| `users.read` | `users.getCurrent()`, `users.list()`, `users.readMembers()` | Reads users and server members. |
| `users.manage` | `users.kick()`, `users.ban()`, `server.registerPluginUser()` | Can create plugin users and moderate members. |
| `roles.read` | `roles.list()` | Reads server roles. |
| `roles.manage` | `roles.setAssignments()`, `users.setRole()` | Changes role assignments or user roles. |
| `messages.read` | `messages.readCurrent()`, message bus latest snapshots | Reads current channel messages. |
| `messages.send` | `messages.send()`, `messages.sendAsPluginUser()` | Sends messages as the current user or registered plugin user. |
| `messages.editOwn` | `messages.edit()` | Edits plugin-owned messages. |
| `messages.deleteOwn` | `messages.delete()` | Deletes plugin-owned messages. |
| `messages.moderate` | `messages.moderateDelete()` | Moderation delete path. |
| `messages.sync` | `messages.sync()` | Syncs message arrays into client state. |
| `channels.read` | `channels.list()`, `channels.select()` | Reads and selects channels. |
| `channels.manage` | `channels.addAudioChannel()`, `channels.addVideoChannel()`, `channels.remove()`, `channels.rename()` | Mutates channel or channel-section state. |
| `server.read` | `server.getCurrent()` | Reads active server. |
| `server.manage` | `server.updatePermissions()`, `server.updateSettings()` | Updates server permissions or settings. |
| `p2p.data` | `p2p.connectedPeers()`, `p2p.broadcastData()`, `p2p.sendData()` | Uses plugin peer data paths. |
| `p2p.media` | Reserved peer media features. | Included for media-facing plugins. |
| `media.playAudio` | `media.playAudioClip()` | Plays an audio URL locally. |
| `media.addAudioStream` | `media.addCustomAudioStream()` | Adds a custom stream to voice handling. |
| `media.addVideoStream` | `media.addCustomVideoStream()` | Registers custom video stream contribution. |
| `audio.volume` | `media.setInputVolume()`, `media.setOutputVolume()` | Adjusts local voice volume. |
| `audio.effects` | Reserved audio effect features. | Included for audio processing plugins. |
| `ui.settings` | `ui.registerSettingsPage()` | Adds settings pages. |
| `ui.pages` | `ui.registerAppPage()`, `ui.registerComposerAction()`, `ui.registerProfileAction()`, `ui.registerToolbarAction()` | Adds app pages and actions. |
| `ui.sidePanel` | `ui.registerSidePanel()` | Adds side panels. |
| `ui.channelsSection` | `ui.registerChannelSection()` | Adds channel sections. |
| `ui.embeds` | `ui.registerEmbedRenderer()` | Renders custom embeds. |
| `ui.dom` | `ui.mountElement()` | Mounts plugin-owned DOM into app targets. |
| `storage.local` | `storage.*`, `clientData.*` | Reads and writes plugin-local data. |
| `storage.serverData.read` | `serverData.read()` | Reads local per-user/per-server plugin data. |
| `storage.serverData.write` | `serverData.write()`, `serverData.remove()` | Writes or removes local per-user/per-server plugin data. |
| `events.server.publish` | `events.publishServer()` | Publishes declared server plugin events. |
| `events.server.subscribe` | `events.subscribeServer()` | Subscribes to declared server plugin events. |
| `events.p2p.publish` | `events.publishP2p()`, `messageBus.publish()`, `messageBus.sendLatestMessages()` | Publishes declared P2P/plugin bus events. |
| `events.p2p.subscribe` | `events.subscribeP2p()`, `messageBus.subscribe()` | Subscribes to declared P2P/plugin bus events. |
## Recommended Practice
Request the fewest capabilities possible. Separate broad features into optional plugin modules when a single plugin would otherwise need many unrelated grants.

View File

@@ -0,0 +1,106 @@
---
sidebar_position: 1
---
# Create a Plugin
MetoYou plugins are browser-safe ES modules loaded by the Angular renderer. A plugin receives a frozen `TojuClientPluginApi`, declares every privileged capability in its manifest, and registers cleanup work through disposables.
## Folder Layout
A local desktop plugin is discovered from an immediate child folder under the app data `plugins` directory.
```text
my-plugin/
toju-plugin.json
main.js
README.md
icon.svg
```
The manifest file can be named `toju-plugin.json` or `plugin.json`. Entrypoints and readmes must stay inside the plugin folder.
## Minimal Manifest
```json
{
"schemaVersion": 1,
"id": "example.hello-world",
"title": "Hello World",
"description": "Adds a toolbar action that sends a message.",
"version": "1.0.0",
"kind": "client",
"scope": "client",
"apiVersion": "1.0.0",
"compatibility": {
"minimumTojuVersion": "1.0.0"
},
"entrypoint": "./main.js",
"capabilities": ["messages.send", "ui.pages"]
}
```
## Entrypoint
```js
export function activate(context) {
const { api } = context;
api.logger.info('Hello World activated');
const disposable = api.ui.registerToolbarAction('hello', {
label: 'Hello',
run: () => api.messages.send('Hello from my plugin')
});
context.subscriptions.push(disposable);
}
export function ready(context) {
context.api.logger.info('All ready plugins have loaded');
}
export function deactivate(context) {
context.api.logger.info('Hello World deactivated');
}
```
## Lifecycle Hooks
| Hook | When it runs | Use it for |
| --- | --- | --- |
| `activate(context)` | During explicit plugin activation. | Register UI, subscribe to events, initialize state. |
| `ready(context)` | After the load-order pass has activated ready plugins. | Cross-plugin coordination that needs other plugins loaded. |
| `deactivate(context)` | During unload or reload. | Flush state and log shutdown. Disposables are also cleaned up by the host. |
| `onPluginDataChanged(context, event)` | When plugin data changes are observed. | React to plugin-scoped persistence changes. |
| `onServerRequirementsChanged(context, snapshot)` | When server plugin requirements change. | Adapt to required, optional, blocked, or incompatible server plugins. |
## Cleanup
Every API registration returns a disposable. Push it into `context.subscriptions`.
```js
const subscription = api.messageBus.subscribe({
topic: 'poll:votes',
handler: (event) => api.logger.info('vote received', event.payload)
});
context.subscriptions.push(subscription);
```
The plugin host disposes subscriptions in reverse order when the plugin unloads.
## Capability Grants
A plugin can only call privileged APIs after the matching capability is declared in the manifest and granted by the user. Keep the manifest narrow. For example, a plugin that only adds a settings page does not need message or user management capabilities.
## Testing Locally
1. Create the plugin folder in the desktop plugins directory.
2. Open the Plugin Manager.
3. Register or refresh local plugins.
4. Grant required capabilities.
5. Activate the plugin.
6. Inspect plugin logs in the manager.
For broad API examples, compare against the E2E fixture plugin under `toju-app/public/plugins/e2e-all-api/`.

View File

@@ -0,0 +1,204 @@
---
sidebar_position: 5
---
# Examples
## Toolbar Message Plugin
`toju-plugin.json`
```json
{
"schemaVersion": 1,
"id": "example.toolbar-message",
"title": "Toolbar Message",
"description": "Adds a toolbar action that sends a reusable message.",
"version": "1.0.0",
"kind": "client",
"scope": "client",
"apiVersion": "1.0.0",
"compatibility": {
"minimumTojuVersion": "1.0.0",
"verifiedTojuVersion": "1.0.0"
},
"entrypoint": "./main.js",
"capabilities": ["messages.send", "ui.pages"]
}
```
`main.js`
```js
export function activate(context) {
const { api } = context;
context.subscriptions.push(api.ui.registerToolbarAction('standup-message', {
label: 'Standup',
run: () => api.messages.send('Standup: yesterday, today, blocked')
}));
}
```
## Settings Page Plugin
```json
{
"schemaVersion": 1,
"id": "example.settings-page",
"title": "Settings Page Example",
"description": "Adds a plugin settings page and stores a local preference.",
"version": "1.0.0",
"kind": "client",
"apiVersion": "1.0.0",
"compatibility": { "minimumTojuVersion": "1.0.0" },
"entrypoint": "./main.js",
"capabilities": ["ui.settings", "storage.local"],
"settings": {
"type": "object",
"properties": {
"enabled": { "type": "boolean", "default": true }
}
}
}
```
```js
export function activate(context) {
const { api } = context;
context.subscriptions.push(api.ui.registerSettingsPage('preferences', {
label: 'Example Preferences',
render: () => {
const root = document.createElement('section');
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Remember preference';
button.onclick = () => api.storage.set('enabled', true);
root.append(button);
return root;
}
}));
}
```
## Server-Scoped Soundboard
A server-scoped plugin can be installed as a server requirement and auto-installed for server members when marked required.
```json
{
"schemaVersion": 1,
"id": "example.soundboard",
"title": "Server Soundboard",
"description": "Adds a soundboard side panel and announces played sounds.",
"version": "1.0.0",
"kind": "client",
"scope": "server",
"apiVersion": "1.0.0",
"compatibility": { "minimumTojuVersion": "1.0.0" },
"entrypoint": "./main.js",
"capabilities": [
"server.read",
"users.manage",
"ui.sidePanel",
"media.playAudio",
"messages.send"
],
"pluginUser": {
"displayName": "Soundboard",
"label": "Audio helper"
}
}
```
```js
export function activate(context) {
const { api } = context;
const botId = api.server.registerPluginUser({
id: 'soundboard-bot',
displayName: 'Soundboard'
});
context.subscriptions.push(api.ui.registerSidePanel('sounds', {
label: 'Soundboard',
render: () => {
const panel = document.createElement('div');
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Play chime';
button.onclick = async () => {
await api.media.playAudioClip({ url: './chime.wav', volume: 0.7 });
api.messages.sendAsPluginUser({ pluginUserId: botId, content: 'Played chime' });
};
panel.append(button);
return panel;
}
}));
}
```
## Message Bus Plugin
```json
{
"schemaVersion": 1,
"id": "example.poll-bus",
"title": "Poll Bus",
"description": "Uses the plugin message bus for lightweight P2P poll votes.",
"version": "1.0.0",
"kind": "client",
"apiVersion": "1.0.0",
"compatibility": { "minimumTojuVersion": "1.0.0" },
"entrypoint": "./main.js",
"capabilities": ["events.p2p.publish", "events.p2p.subscribe", "messages.read"]
}
```
```js
export function activate(context) {
const { api } = context;
context.subscriptions.push(api.messageBus.subscribe({
topic: 'poll:votes',
replayLatest: true,
latestMessageLimit: 20,
handler: (event) => api.logger.info('Vote received', event.payload)
}));
api.messageBus.publish({
topic: 'poll:votes',
payload: { option: 'A' },
includeLatestMessages: true,
includeSelf: true,
latestMessageLimit: 20
});
}
```
## Custom DOM Mount
Use `ui.dom` sparingly and cleanly. The runtime tags mounted elements with plugin ownership metadata and removes remaining mounted elements when the plugin unloads.
```js
export function activate(context) {
const badge = document.createElement('div');
badge.textContent = 'Plugin active';
badge.style.position = 'absolute';
badge.style.right = '1rem';
badge.style.bottom = '1rem';
context.subscriptions.push(context.api.ui.mountElement('active-badge', {
target: 'body',
element: badge
}));
}
```
## All-API Fixture
The repo includes an E2E fixture at `toju-app/public/plugins/e2e-all-api/`. It intentionally calls every public plugin API surface so Playwright coverage can validate the runtime. Use it as a compatibility reference, not as the minimal style for production plugins.

View File

@@ -0,0 +1,162 @@
---
sidebar_position: 2
---
# Manifest Model
The manifest is the source of truth for plugin identity, compatibility, runtime shape, capabilities, data, events, UI hints, and distribution metadata.
```ts
type TojuPluginInstallScope = 'client' | 'server';
type PluginEventDirection = 'clientToServer' | 'serverRelay' | 'p2pHint';
type PluginEventScope = 'server' | 'channel' | 'user' | 'plugin';
type PluginCapabilityId =
| 'profile.read'
| 'profile.write'
| 'users.read'
| 'users.manage'
| 'roles.read'
| 'roles.manage'
| 'messages.read'
| 'messages.send'
| 'messages.editOwn'
| 'messages.deleteOwn'
| 'messages.moderate'
| 'messages.sync'
| 'channels.read'
| 'channels.manage'
| 'server.read'
| 'server.manage'
| 'p2p.data'
| 'p2p.media'
| 'media.playAudio'
| 'media.addAudioStream'
| 'media.addVideoStream'
| 'audio.volume'
| 'audio.effects'
| 'ui.settings'
| 'ui.pages'
| 'ui.sidePanel'
| 'ui.channelsSection'
| 'ui.embeds'
| 'ui.dom'
| 'storage.local'
| 'storage.serverData.read'
| 'storage.serverData.write'
| 'events.server.publish'
| 'events.server.subscribe'
| 'events.p2p.publish'
| 'events.p2p.subscribe';
interface TojuPluginManifest {
schemaVersion: 1;
id: string;
title: string;
description: string;
version: string;
kind: 'client' | 'library';
scope?: TojuPluginInstallScope;
apiVersion: string;
compatibility: {
minimumTojuVersion: string;
maximumTojuVersion?: string;
verifiedTojuVersion?: string;
};
entrypoint?: string;
bundle?: {
url: string;
entrypoint?: string;
};
readme?: string;
homepage?: string;
bugs?: string;
changelog?: string;
license?: string;
authors?: {
name: string;
email?: string;
url?: string;
}[];
capabilities?: PluginCapabilityId[];
events?: {
eventName: string;
direction: PluginEventDirection;
scope: PluginEventScope;
maxPayloadBytes?: number;
schema?: string;
}[];
data?: {
key: string;
schema?: string;
scope: string;
storage: 'local' | 'serverData';
}[];
relationships?: {
after?: string[];
before?: string[];
conflicts?: string[];
optional?: { id: string; versionRange?: string }[];
requires?: { id: string; versionRange?: string }[];
};
load?: {
priority?: 'bootstrap' | 'high' | 'default' | 'low';
};
pluginUser?: {
avatar?: string;
displayName: string;
label?: string;
};
settings?: Record<string, unknown>;
ui?: Record<string, unknown>;
}
```
## Required Fields
| Field | Meaning |
| --- | --- |
| `schemaVersion` | Manifest schema version. Currently `1`. |
| `id` | Stable plugin id. Use a reverse-DNS or package-style id. |
| `title` | Human-readable plugin name. |
| `description` | Short explanation shown in plugin UI. |
| `version` | Plugin version. |
| `kind` | `client` for runtime plugins, `library` for shared dependency-style entries. |
| `apiVersion` | Plugin API version expected by the plugin. |
| `compatibility.minimumTojuVersion` | Oldest app version the plugin supports. |
## Scope
`scope: "client"` installs the plugin for the current client. Omit `scope` for the same behavior.
`scope: "server"` marks a plugin as server-scoped. Server-scoped store entries can be installed to a chat server as requirements. Required server plugins are auto-installed for members when that server opens; optional requirements stay listed but do not auto-install.
## Entrypoint and Bundle
Use `entrypoint` for a browser-resolvable module relative to the manifest. Use `bundle.url` when publishing a cached browser bundle through a plugin source manifest. Desktop installs cache bundle files into app data and load the cached manifest afterward.
## Events
Every server or P2P plugin event should be declared before it is published or subscribed to.
```json
{
"events": [
{
"eventName": "poll:vote",
"direction": "p2pHint",
"scope": "channel",
"maxPayloadBytes": 2048
}
]
}
```
## Data Declarations
Use `data` to document plugin-owned data keys and intended storage.
- `local` maps to client-local plugin data.
- `serverData` maps to local per-user/per-server plugin data.
Signal server HTTP persistence for arbitrary plugin data is disabled by design.