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
| Concept | What it is |
|---|
| Squad | A private community hub containing participants and MLS-backed channels. Stable id = announcements MLS group id. |
| Squad-pair | Partner 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. |
| Network | A “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.
| Moment | What to do |
|---|
| Create parent | After the announcements MLS group is created, set squad.id = groupId (the returned hex group id). |
| Accept invite | Set squad.id = payload.groupId (the announcements group id carried in the invite payload). |
| Backend → UI event | channel_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:
| Channel | Purpose |
|---|
| dashboard | Squad overview: governance, treasury, settings tabs |
| announcements | The first channel; its MLS group id is the squad’s stable id |
| inbox | Pinned invite cards and notifications |
| polls | Governance 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
| Event | When it fires | What to do |
|---|
mls_invite_received | A new Gift Wrap containing an MlsWelcome (kind 443) arrives | Refresh the list_pending_mls_welcomes list |
mls_group_initial_sync | The creator’s local state is ready after group creation | Show the new squad/channel in the sidebar |
mls_group_updated | Member list or group metadata changed | Re-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.
| Location | Key / table | Purpose |
|---|
SQLite squads | Primary key = announcements MLS group id | Full squad catalog: name, kind, visibility, commons_tags, paired_squads, channels JSON |
localStorage | pacto_last_squad_id_<npub> | Last opened squad or squad-pair |
localStorage | pacto_last_channel_by_squad_<npub> | Per-parent last opened channel |
SQLite catalog commands
| Command | Role |
|---|
list_squads | Hydrate the frontend squads store on login |
upsert_squad | Create or update a squad row |
get_squad | Look up a squad by parentId |
delete_squad | Exit a squad: remove the row and all roster bindings for parent_id |
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.
| Setting | Behavior |
|---|
| Private (default) | No Commons broadcast menu; not visible in the feed |
| Public | 1–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:
- MLS leave (engine-level group exit)
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.