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.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.
Transport and kinds
| Path | Wire | Inner (after decrypt / MLS process) |
|---|---|---|
| DM | Kind 1059 — Gift Wrap (NIP-59), addressed to the recipient | Rumors: 14 (text), 15 (files), 7 (reactions), 30078 (typing), etc. |
| MLS invite | Kind 443 (MlsWelcome) inside a Gift Wrap | Handed to the MLS engine; not processed as a normal DM rumor |
| MLS group traffic | Kind 444 (MlsGroupMessage), h tag = wire group id | Same 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/ UIchat_idis the other user’s npub (npub1…). - MLS group:
chat_idis the group id — a hex string taken from thehtag. It does not start withnpub1.
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.
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:
-
Gift Wrap (kind 1059) — Unwrap the outer seal. If the inner event is
MlsWelcome(kind 443), hand it to the MLS engine and emitmls_invite_received. Otherwise treat it as a DM rumor: callprocess_rumorwithConversationType::DirectMessage, key storage by the sender’s npub, and emitmessage_new. -
MlsGroupMessage(kind 444) — Read thehtag to identify the group. Verify membership viadb::load_mls_groups. Process with the MLS engine on a blocking thread (see Threading below). Callprocess_rumorwithConversationType::MlsGroupand emitmls_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_nowruns a cursor-based sync for each group. Per-group cursors in themls_event_cursorstable track the last seen kind 444 event, enabling incremental backfill.
State
| Layer | Key | Contents |
|---|---|---|
| In-memory | STATE (ChatState) | DMs keyed by npub; MLS groups by group_id |
| Durable | SQLite events table | Decrypted, PIN-encrypted content; chat/profile rows |
Relay configuration
Relay lists and trusted relay sets are configured in Rust. The constantTRUSTED_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 constantadd_relay— where relays are registered with the Nostr client- subscription filters — where event kinds and authors are specified for each subscription
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’sinvoke / listen bridge.
Events emitted from Rust → frontend:
| Event | Trigger |
|---|---|
message_new | DM rumor processed and stored |
mls_message_new | MLS group message decrypted and stored |
mls_invite_received | Kind 443 welcome unwrapped and processed |
init_finished | Backend init and initial sync complete |
typing-update | Typing indicator for a conversation (npub or group_id) |
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.