Documentation Index
Fetch the complete documentation index at: https://mintlify.com/covenant-gov/pacto-app/llms.txt
Use this file to discover all available pages before exploring further.
Pacto uses the MDK MLS engine (mdk_core / mdk_sqlite_storage) for all encrypted group communication. Each group is identified by a hex group_id (the h tag on Kind 444 wire events). The commands on this page cover the full lifecycle of an MLS group: creation, invite delivery and acceptance, membership management, sync, and voluntary or forced departure. Because the MDK engine is not Send, all engine calls run inside tokio::task::spawn_blocking — from the frontend’s perspective these are ordinary async invoke calls.
Import
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
Group lifecycle commands
create_group_chat
Create a new MLS group from a display name and a list of member npubs. For each npub, the backend refreshes the member’s device KeyPackages from trusted relays before group creation. If a member has no available KeyPackage, that member is skipped; if all selected members are missing KeyPackages, creation aborts.
| Parameter | Type | Description |
|---|
groupName | string | Display name for the new group (must not be empty) |
memberIds | string[] | Array of member npubs to invite on creation |
Returns: string — the group_id hex string of the created group.
Emits: mls_group_initial_sync when the creator’s initial sync completes.
const groupId = await invoke('create_group_chat', {
groupName: 'Neo Builders',
memberIds: ['npub1alice...', 'npub1bob...']
});
On success the backend automatically emits mls_group_initial_sync so the group list view updates without requiring a manual refresh.
create_mls_group
Lower-level group creation that accepts explicit (device_id, keypackage_ref) tuples instead of resolving them from npubs. Use create_group_chat for typical flows; prefer this command when you need fine-grained control over which device is added per member.
| Parameter | Type | Description |
|---|
name | string | Display name for the group |
avatarRef | string | null | Optional avatar reference string |
initialMemberDevices | [string, string][] | Array of [device_id, keypackage_ref] tuples |
Returns: string — group_id hex.
list_mls_groups
List all MLS groups the current device has joined. Groups marked as evicted in the local database are excluded from the result.
Parameters: none
Returns: string[] — array of group_id hex strings.
const groupIds = await invoke('list_mls_groups');
Retrieve full metadata for all non-evicted MLS groups. Each entry includes the group name, description, member count, and other display fields.
Parameters: none
Returns: object[] — JSON array of group metadata objects (one per non-evicted group).
const groups = await invoke('get_mls_group_metadata');
for (const group of groups) {
console.log(group.group_id, group.group_name);
}
load_mls_device_id
Retrieve this device’s MLS device identifier from the persistent engine.
Parameters: none
Returns: string | null — device ID string, or null if not yet initialised.
const deviceId = await invoke('load_mls_device_id');
load_mls_keypackages
Return the cached MLS KeyPackages for this device. Used internally to verify that the device has valid published KeyPackages before sending or receiving invites.
Parameters: none
Returns: object[] — array of KeyPackage JSON objects.
const keypackages = await invoke('load_mls_keypackages');
console.log(`${keypackages.length} cached KeyPackages`);
Invite commands
list_pending_mls_welcomes
Return all pending MLS Welcome events (invites) that have been received but not yet accepted. Each entry is a SimpleWelcome object.
Parameters: none
Returns: SimpleWelcome[]
SimpleWelcome shape:
| Field | Type | Description |
|---|
id | string | Inner welcome event ID hex — pass this to accept_mls_welcome |
wrapperEventId | string | Gift Wrap outer event ID hex (dedup key, not for accepting) |
nostrGroupId | string | Group ID hex |
groupName | string | Human-readable group name |
groupDescription | string | null | Optional group description |
groupAdminPubkeys | string[] | Admin npubs or hex pubkeys |
groupRelays | string[] | Relay URLs associated with the group |
welcomer | string | npub of the user who sent the invite |
memberCount | number | Current member count |
const welcomes = await invoke('list_pending_mls_welcomes');
// welcomes[0].id ← use this for accept
// welcomes[0].wrapperEventId ← do NOT pass this to accept
accept_mls_welcome
Accept a pending MLS invite and join the group. After a successful accept, the backend automatically regenerates the device KeyPackage.
| Parameter | Type | Description |
|---|
welcomeEventIdHex | string | SimpleWelcome.id (inner welcome event ID) |
Returns: boolean — true if accepted successfully, false if the welcome was not found.
Always pass SimpleWelcome.id, not wrapperEventId. The inner welcome event ID and the outer Gift Wrap ID are different values. Passing the wrong ID will cause the accept call to fail or silently return false.
const welcomes = await invoke('list_pending_mls_welcomes');
// Correct: use .id (inner event id)
const accepted = await invoke('accept_mls_welcome', {
welcomeEventIdHex: welcomes[0].id
});
// Wrong: do NOT use .wrapperEventId
// await invoke('accept_mls_welcome', {
// welcomeEventIdHex: welcomes[0].wrapperEventId // ← this will fail
// });
invite_member_to_group
Add an existing contact to an already-created MLS group. Refreshes the member’s device KeyPackages, selects the first available device, adds it to the group via the MLS engine, publishes a commit, and sends a Welcome via Gift Wrap to the invitee.
| Parameter | Type | Description |
|---|
groupId | string | hex group_id of the target group |
memberNpub | string | npub of the member to invite |
Returns: void
Emits: mls_group_updated after the participant list is updated.
await invoke('invite_member_to_group', {
groupId: 'a1b2c3d4...',
memberNpub: 'npub1charlie...'
});
Membership commands
remove_mls_member_device
Remove a member’s device from an MLS group (kick). The MLS engine enforces admin policy — only the group creator is an admin by default. After removal, publishes an evolution event and merges the pending commit locally.
| Parameter | Type | Description |
|---|
groupId | string | hex group_id |
memberNpub | string | npub of the member to remove |
deviceId | string | Device ID of the specific device to remove |
Returns: void
Emits: mls_group_updated after the participant list is synced.
await invoke('remove_mls_member_device', {
groupId: 'a1b2c3d4...',
memberNpub: 'npub1badactor...',
deviceId: 'device-hex-id'
});
The evicted user has no special notification. They discover eviction from engine errors (“own leaf not found”) on their next send or sync attempt. Their client will mark the group as evicted and emit mls_group_left.
leave_mls_group
Voluntarily leave an MLS group. Publishes a leave proposal to the group, removes the group from local metadata immediately, and emits mls_group_left. The group continues for all remaining members; the MLS tree updates without the leaver.
| Parameter | Type | Description |
|---|
groupId | string | hex group_id to leave |
Returns: void
Emits: mls_group_left
await invoke('leave_mls_group', { groupId: 'a1b2c3d4...' });
Admin handoff: Pacto does not currently expose “add admin” or “transfer admin” for MLS groups. Only the creator holds admin rights at creation. If the creator leaves, no further member removal is possible from MLS until admin update support is added.
sync_all_profiles
Trigger a profile sync for all known contacts — fetches the latest Kind 0 metadata from relays and updates the local profile cache.
Parameters: none
Returns: void
await invoke('sync_all_profiles');
sync_mls_groups_now
Trigger an MLS group sync. Optionally scoped to a single group by passing its group_id; if groupId is null, all joined groups are synced. Fetches Kind 444 history per group using stored event cursors and processes any new messages.
| Parameter | Type | Description |
|---|
groupId | string | null | Group to sync, or null to sync all groups |
Returns: [number, number] — [processed, skipped] event counts.
// Sync all groups
await invoke('sync_mls_groups_now', { groupId: null });
// Sync one group
await invoke('sync_mls_groups_now', { groupId: 'a1b2c3d4...' });
Cursor commands
list_group_cursors
List the MLS event sync cursors per group. Each cursor tracks the last seen Nostr event for a group, enabling efficient backfill on reconnect.
Parameters: none
Returns: object — JSON map of group_id → cursor values.
const cursors = await invoke('list_group_cursors');
Dashboard polls
list_dashboard_polls
Retrieve all dashboard polls for a parent (squad) group. Polls are replicated through the parent’s #announcements MLS channel as Kind 30078 rumors.
| Parameter | Type | Description |
|---|
parentId | string | group_id of the parent squad |
Returns: DashboardPollDto[] — array of poll objects ordered by creation time.
const polls = await invoke('list_dashboard_polls', {
parentId: 'squad-group-id-hex'
});
Tauri events
mls_invite_received
Emitted when a new MLS Welcome (Kind 443 inside a Gift Wrap) is processed during sync. After receiving this event, refresh the pending invites list.
Payload: { wrapperEventId: string }
const unlisten = await listen('mls_invite_received', async () => {
const welcomes = await invoke('list_pending_mls_welcomes');
// Update UI with new pending invites
});
mls_message_new
Emitted when a new application message is decrypted from a Kind 444 group message.
Payload: { group_id: string, message: Message }
const unlisten = await listen('mls_message_new', (event) => {
const { group_id, message } = event.payload;
console.log(`New group message in ${group_id}:`, message.content);
});
mls_group_left
Emitted when this client has left a group — either voluntarily via leave_mls_group or as a result of eviction detected during sync.
Payload: { group_id: string }
const unlisten = await listen('mls_group_left', (event) => {
const { group_id } = event.payload;
// Remove the group from the UI
});
mls_group_updated
Emitted when the member list or group metadata changes — after a successful remove_mls_member_device, invite_member_to_group, or internal participant sync.
Payload: { group_id: string }
const unlisten = await listen('mls_group_updated', (event) => {
const { group_id } = event.payload;
// Refresh member list for this group
});
mls_group_initial_sync
Emitted by the creator’s client after the group is first created and the initial sync completes. Use this to add the new group to the UI without requiring a full list refresh.
Payload: { group_id: string }
const unlisten = await listen('mls_group_initial_sync', (event) => {
const { group_id } = event.payload;
// Add the new group to the sidebar
});
Invite accept flow example
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
// 1. Refresh pending invites when a new one arrives
const unlisten = await listen('mls_invite_received', async () => {
const welcomes = await invoke('list_pending_mls_welcomes');
console.log(`${welcomes.length} pending invite(s)`);
if (welcomes.length > 0) {
const invite = welcomes[0];
console.log(`Invite from ${invite.welcomer} to join "${invite.groupName}"`);
// 2. Accept using .id — NOT .wrapperEventId
const ok = await invoke('accept_mls_welcome', {
welcomeEventIdHex: invite.id // ← always .id
});
if (ok) {
console.log('Joined group:', invite.nostrGroupId);
}
}
});
// 3. Listen for messages in the joined group
const msgUnlisten = await listen('mls_message_new', (event) => {
const { group_id, message } = event.payload;
console.log(`[${group_id}] ${message.content}`);
});
accept_mls_welcome takes SimpleWelcome.id, not wrapperEventId. These two fields look similar (both are hex event IDs) but refer to different events. Always pass SimpleWelcome.id (the inner welcome rumor ID). Using wrapperEventId will silently fail or return false.
Pending proposal errors: if the engine logs Can't create message because a pending proposal exists or Unprocessable event, a proposal (from a prior add/remove) has not yet been committed. Resolution options: wait for another member to commit, restart and re-sync, or call sync_mls_groups_now. Pacto automatically merges pending commits after remove_mls_member_device and invite_member_to_group, but not necessarily after every leave_mls_group.