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.

Nostr in Pacto is a transport layer, not a storage layer. The protocol carries encrypted event envelopes between clients via relays; all decrypted content lands in per-account SQLite and is never stored in plaintext on a relay. Direct messages travel as NIP-59 Gift Wraps (kind 1059), giving them sealed-sender privacy. MLS group traffic uses its own wire kinds — kind 443 for welcome invites and kind 444 for group messages — while still leveraging the relay network for delivery.

Transport and kinds

PathWireInner (after decrypt / MLS process)
DMKind 1059 — Gift Wrap (NIP-59), addressed to the recipientRumors: 14 (text), 15 (files), 7 (reactions), 30078 (typing), etc.
MLS inviteKind 443 (MlsWelcome) inside a Gift WrapHanded to the MLS engine; not processed as a normal DM rumor
MLS group trafficKind 444 (MlsGroupMessage), h tag = wire group idSame rumor kinds as DMs once decrypted by MDK

Conversation identity

How a conversation is identified determines which code path handles it end-to-end:
  • DM: chat_identifier / UI chat_id is the other user’s npub (npub1…).
  • MLS group: chat_id is the group id — a hex string taken from the h tag. It does not start with npub1.
ChatType in src-tauri/src/chat.rs carries the DirectMessage vs MlsGroup variant and is checked throughout the backend whenever routing decisions are needed.

Backend flow

Sending (message.rs)

The entry points — message (text), file_message, voice_message, and related commands — all accept a receiver parameter that drives the routing decision:
  • If a chat already exists, chat.is_mls_group() selects the MLS branch.
  • If no chat exists yet, receiver.starts_with("npub1") implies the DM path.
DM path: Build the inner rumor → wrap in a NIP-59 Gift Wrap addressed to the peer’s pubkey → publish via the Nostr client. MLS path: Build the same inner rumor kinds → call crate::mls::send_mls_message → publish as kind 444 on the wire with the correct group reference.

Receiving (lib.rs)

Two live subscription paths run in parallel:
  1. Gift Wrap (kind 1059) — Unwrap the outer seal. If the inner event is MlsWelcome (kind 443), hand it to the MLS engine and emit mls_invite_received. Otherwise treat it as a DM rumor: call process_rumor with ConversationType::DirectMessage, key storage by the sender’s npub, and emit message_new.
  2. MlsGroupMessage (kind 444) — Read the h tag to identify the group. Verify membership via db::load_mls_groups. Process with the MLS engine on a blocking thread (see Threading below). Call process_rumor with ConversationType::MlsGroup and emit mls_message_new.

Sync

  • DM sync: Fetch Gift Wraps addressed to self; pass each through handle_event, which runs the same unwrap and routing logic as the live path.
  • MLS sync: After DM sync and init complete, sync_mls_groups_now runs a cursor-based sync for each group. Per-group cursors in the mls_event_cursors table track the last seen kind 444 event, enabling incremental backfill.

State

LayerKeyContents
In-memorySTATE (ChatState)DMs keyed by npub; MLS groups by group_id
DurableSQLite events tableDecrypted, PIN-encrypted content; chat/profile rows

Relay configuration

Relay lists and trusted relay sets are configured in Rust. The constant TRUSTED_RELAYS defines the set used for MLS welcomes and other sensitive operations. When changing relay behavior, grep the Rust source for:
  • TRUSTED_RELAYS — the trusted relay constant
  • add_relay — where relays are registered with the Nostr client
  • subscription filters — where event kinds and authors are specified for each subscription
User-configurable relay preferences are stored in the settings table and are loaded at login.

Frontend

The frontend does not interact with Nostr directly. It communicates with the Rust backend exclusively through Tauri’s invoke / listen bridge. Events emitted from Rust → frontend:
EventTrigger
message_newDM rumor processed and stored
mls_message_newMLS group message decrypted and stored
mls_invite_receivedKind 443 welcome unwrapped and processed
init_finishedBackend init and initial sync complete
typing-updateTyping indicator for a conversation (npub or group_id)
The central listener in src/lib/app/tauri-subscriptions.ts subscribes to all backend events and fans them out to the appropriate Svelte stores.
Files not to confuse: src/lib/api/nostr.ts is the thin TypeScript API surface that wraps invoke calls from the web layer — it is not the Rust Nostr implementation. The Rust side lives in src-tauri/src/nostr/, lib.rs, and message.rs. Similarly, stored_event.rs contains helpers for the kind/content/tags shape used when persisting or displaying protocol-aligned data.

Build docs developers (and LLMs) love