From b1540f0608ea4420973040d6775e7fdd946741c8 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sat, 26 Sep 2026 19:56:58 +0700 Subject: [PATCH] =?UTF-8?q?docs:=20group=20messaging=20=E2=80=94=20what=20?= =?UTF-8?q?ships=20(NIP-17)=20and=20the=20upgrade=20paths=20(Marmot/Concor?= =?UTF-8?q?d/Cordn)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GROUP_MESSAGING.md | 60 ++++++++++++++++++++++++++++++++++ src/hooks/useDirectMessages.ts | 1 + 2 files changed, 61 insertions(+) create mode 100644 GROUP_MESSAGING.md diff --git a/GROUP_MESSAGING.md b/GROUP_MESSAGING.md new file mode 100644 index 0000000..1a84004 --- /dev/null +++ b/GROUP_MESSAGING.md @@ -0,0 +1,60 @@ +# Group Messaging + +## What ships today (NIP-17 group DMs) + +SkateSpots group chat uses the **NIP-17 fan-out primitive** — the same +encryption path as 1:1 DMs, just addressed to more than one person: + +- The sender builds one **kind-14 rumor** with a `p` tag per recipient, then + encrypts that same rumor **to each recipient separately** inside a kind-13 + seal, wrapped in a kind-1059 gift wrap addressed only to them. +- Anyone receiving a rumor with **more than one `p` tag** is in a group. +- A group is identified by `getGroupId(participants)` — the sorted, joined + participant pubkeys — so every client derives the same id. + +Implementation: + +| Piece | File | +| --- | --- | +| Send (1:1 or group) | `useDirectMessages.ts` → `useSendDirectMessage` (`createRumor`) | +| List groups (inbox) | `useDirectMessages.ts` → `useGroupConversations` | +| Group thread | `useDirectMessages.ts` → `useGroupConversation(participants)` | +| Thread UI | `pages/GroupConversationPage.tsx` → `/messages/group/:participants` | +| Create a group | `components/NewMessageDialog.tsx` (multi-select → *Start group*) | + +## Honest limitations + +This primitive is simple and interoperable, but it is **not** a modern group +protocol: + +- **No forward secrecy / post-compromise security.** One leaked message key can + expose that message (and, with a leaked identity key, the whole history). +- **No membership ratchet or roster.** "Membership" is just whoever is in the + `p` tags; there is no cryptographic join/leave, and no automatic rekey when + someone is removed. +- **Fan-out cost** is O(n) per message (one sealed copy per recipient). +- **Metadata** (timing, group size) is visible to relays. + +Good enough for casual crew/community chats. Not appropriate for high-stakes or +large public rooms. + +## Upgrade paths (when the guarantees matter) + +From *Encrypted Group Chat on Nostr: Marmot, Concord, and Cordn Compared* +(): + +| Protocol | Crypto | Use when | +| --- | --- | --- | +| **Marmot** | MLS (RFC 9420), no privileged server | Small, high-stakes groups needing forward secrecy + post-compromise security; the most mature spec and a flagship Rust implementation (MDK) | +| **Concord** | Shared key + NIP-59 giftwrap, signed roster | Large, public, Discord-style communities (channels/roles/moderation). Trades forward secrecy for scale | +| **Cordn** | MLS via a per-group coordinator (ContextVM) | MLS guarantees where running/finding a small self-hostable coordinator is acceptable; best public-metadata story | + +Migration would be additive: keep the NIP-17 threads for casual DMs and adopt one +of the above for rooms that need ratcheting, rosters and moderation. Revisit when +the target protocol's spec has stabilised. + +## Decision + +Ship NIP-17 group DMs now (zero new protocols, reuses the DM stack); adopt +**Marmot** if/when teams need Signal-grade guarantees, and **Concord** if the goal +becomes large public communities. See the article above for the trade-offs. diff --git a/src/hooks/useDirectMessages.ts b/src/hooks/useDirectMessages.ts index 511acfa..85c2d61 100644 --- a/src/hooks/useDirectMessages.ts +++ b/src/hooks/useDirectMessages.ts @@ -504,6 +504,7 @@ export function useConversations() { * Scope note: this is fan-out share-the-rumor group messaging. It has no * forward secrecy or membership ratcheting/roster — Marmot (MLS) and Cordn are * the protocols to adopt if those guarantees are required later. + * See GROUP_MESSAGING.md for the full decision and upgrade paths. */ export function useGroupConversations() { const { nostr } = useNostr();