docs: group messaging — what ships (NIP-17) and the upgrade paths (Marmot/Concord/Cordn)

This commit is contained in:
SkateSpots 2026-09-26 19:56:58 +07:00
parent adc3a20094
commit b1540f0608
2 changed files with 61 additions and 0 deletions

60
GROUP_MESSAGING.md Normal file
View file

@ -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*
(<https://nostrdev.com/blog/nostr-group-chat-protocols/>):
| 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.

View file

@ -504,6 +504,7 @@ export function useConversations() {
* Scope note: this is fan-out share-the-rumor group messaging. It has no * 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 * forward secrecy or membership ratcheting/roster — Marmot (MLS) and Cordn are
* the protocols to adopt if those guarantees are required later. * the protocols to adopt if those guarantees are required later.
* See GROUP_MESSAGING.md for the full decision and upgrade paths.
*/ */
export function useGroupConversations() { export function useGroupConversations() {
const { nostr } = useNostr(); const { nostr } = useNostr();