Skip to main content

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.

Squads are Pacto’s private community hubs — think Discord servers, but backed by Messaging Layer Security (MLS) instead of server-side encryption. Every squad contains one or more channels, each backed by its own MLS group. Members exchange messages over kind 444 (MlsGroupMessage) events on TRUSTED_RELAYS; the MLS engine on each device handles key agreement, forward secrecy, and membership changes without a trusted server. Squad invites travel inside NIP-59 Gift Wraps (kind 1059) to preserve metadata privacy until the recipient accepts.

Concepts: Squad, Squad-pair, and Network

ConceptWhat it is
SquadA private community hub containing participants and MLS-backed channels. Stable id = announcements MLS group id.
Squad-pairPartner coordination parent linking exactly two anchor squads (pairedSquads). Listed under Partner Squads in the anchor squad sidebar. Uses the same announcements-group-id identity rule.
NetworkA “Squad of Squads” — an inter-organizational hub for cross-group coordination. Created via the same MLS + channel stack.
All three share the same underlying architecture: a parent entity whose permanent identifier is the MLS group id of its announcements channel.

Stable identity: the announcements group id

Every device and every backend event refers to a squad using a single, stable string: the MLS group id of the announcements channel (a hex string returned by the backend when that MLS group is created). This is what makes it possible for events like channel_added_to_squad to reliably find the right parent across devices and restarts.
MomentWhat to do
Create parentAfter the announcements MLS group is created, set squad.id = groupId (the returned hex group id).
Accept inviteSet squad.id = payload.groupId (the announcements group id carried in the invite payload).
Backend → UI eventchannel_added_to_squad and similar events carry announcements_group_id. Resolve the parent with squads.find(s => s.id === announcements_group_id).
Structured DMs that reference a squad (such as squad invites or Commons join requests) must also use the announcements MLS group id as the squad identifier — never a per-device UUID or channel-specific group id.

Default channels

When a squad is created or a member accepts an invite, four default channels are provisioned by defaultChannelRowsForGroupId in src/lib/parent-navbar.ts:
ChannelPurpose
dashboardSquad overview: governance, treasury, settings tabs
announcementsThe first channel; its MLS group id is the squad’s stable id
inboxPinned invite cards and notifications
pollsGovernance polls and voting
Channels appear in sidebar order (ascending order field). The announcements channel is always channels[0] by order.
The Dashboard channel is a special composite view with four internal tabs: Dashboard (overview), Governance (proposals), Treasury (wallet/Safe), and Settings. It is not a chat stream.

Invite flow

Listing pending invites

import { invoke } from '@tauri-apps/api/core';

// Returns SimpleWelcome[] — one row per pending MLS Welcome
const pendingInvites = await invoke('list_pending_mls_welcomes');
// Each row has: id (inner welcome event id), wrapper_event_id, groupId, …

Accepting an invite

import { invoke } from '@tauri-apps/api/core';

// IMPORTANT: pass SimpleWelcome.id (inner welcome event id),
// NOT wrapper_event_id
await invoke('accept_mls_welcome', {
  welcomeEventIdHex: simpleWelcome.id
});
Always pass SimpleWelcome.id (the inner welcome event id) to accept_mls_welcome, not wrapper_event_id. Passing the wrapper id will cause the accept to fail silently.

Invite events

EventWhen it firesWhat to do
mls_invite_receivedA new Gift Wrap containing an MlsWelcome (kind 443) arrivesRefresh the list_pending_mls_welcomes list
mls_group_initial_syncThe creator’s local state is ready after group creationShow the new squad/channel in the sidebar
mls_group_updatedMember list or group metadata changedRe-render the member list

Sending and receiving messages

Sending to a group channel

import { invoke } from '@tauri-apps/api/core';

// receiver = hex group_id (NOT npub — MLS groups never start with npub1)
await invoke('message', {
  receiver: groupId,
  content: 'Hello squad!',
  repliedTo: null  // or message id string to thread a reply
});
The backend detects that groupId does not start with npub1 and routes through crate::mls::send_mls_message → kind 444 on TRUSTED_RELAYS.

Receiving group messages

import { listen } from '@tauri-apps/api/event';

// Fires for every new decrypted MLS group message
await listen('mls_message_new', (event) => {
  // event.payload.group_id === groupId (hex)
  // event.payload.message contains the decrypted rumor
  console.log(event.payload.group_id, event.payload.message);
});
Use invoke('list_mls_groups') on app init to hydrate the groups list. The init_finished event also provides chats with chat_type: 'MlsGroup' for all joined groups.

Squad catalog persistence

The full squad catalog (name, channels, kind, visibility, tags, squad-pair metadata) is persisted in SQLite (vector.db) in the squads table, not in localStorage. Navigation preferences use scoped localStorage keys.
LocationKey / tablePurpose
SQLite squadsPrimary key = announcements MLS group idFull squad catalog: name, kind, visibility, commons_tags, paired_squads, channels JSON
localStoragepacto_last_squad_id_<npub>Last opened squad or squad-pair
localStoragepacto_last_channel_by_squad_<npub>Per-parent last opened channel

SQLite catalog commands

CommandRole
list_squadsHydrate the frontend squads store on login
upsert_squadCreate or update a squad row
get_squadLook up a squad by parentId
delete_squadExit a squad: remove the row and all roster bindings for parent_id

Squad-pair metadata

A squad-pair is a coordination parent linking exactly two anchor squads. Its TypeScript type shape:
export type SquadKind = 'squad' | 'squad-pair';

export interface Squad {
  id: string;           // announcements MLS group id
  name: string;
  kind: SquadKind;
  pairedSquads?: [
    { id: string; name: string },
    { id: string; name: string }
  ];
  channels: Channel[];  // channels[0] (by order) = announcements
  // ...
}
When kind === 'squad-pair', pairedSquads contains exactly two entries — one for each anchor squad. Squad-pairs are created via Pair with squad… from an anchor squad header.

Commons visibility

Squads default to private at creation. A private squad has no Commons metadata and never appears in the discovery feed.
SettingBehavior
Private (default)No Commons broadcast menu; not visible in the feed
Public1–3 hashtags chosen at creation; automatic 72-hour #new broadcast on create; Broadcast Squad option appears in the squad header menu
Tag choices and visibility are stored in the squads table (visibility, commons_tags). See Commons Discovery for the full broadcast flow.

Member management

Inviting a member

Inviting a user requires that they have a published KeyPackage on TRUSTED_RELAYS. The backend command invite_member_to_group fetches the invitee’s KeyPackage, calls add_members on the MLS engine, publishes a commit, and sends a Gift-Wrapped Welcome (kind 443) to the invitee.

Voluntary leave

A member can leave a squad via the frontend exit-parent-flow. This triggers:
  1. MLS leave (engine-level group exit)
  2. delete_squad command (removes the SQLite row and roster bindings for parent_id)
EVM accounts, squad infrastructure, and on-chain treasury history are not deleted — keys may hold funds or be bound to other squads.

Eviction (creator-only)

Only the squad creator can evict a member in v1. Eviction updates the MLS group tree via a commit, removing the evicted member’s leaf. Evicted members can be re-invited; re-inviting clears any “evicted” state in the local MLS database before the new Welcome is processed.

Build docs developers (and LLMs) love