Nostrautica docs

Multi-device Marmot chat — feasibility study and target design

Date: 2026-07-22 · Verified against: main @ 3bf4cfa (all file:line references checked against the working tree). Companion to PROTOCOL-NIP.md §10, which carries the normative wire spec; this document carries the feasibility analysis, the change list, and the migration plan.

0. Goal

The same npub logged in on mobile and desktop at the same time, both participating in the event's Marmot/MLS group chat. Synced history across devices is explicitly a non-goal — MLS state is per-leaf and each device reads from its own join epoch forward. That constraint is native to MLS and we embrace it rather than fight it.

Verdict up front: feasible with the vendored marmot-ts/ts-mls as-is — no library change required. The approach is per-device chat keys: every browser/device mints its own chat keypair and is attested to the account; each device is an ordinary, independent MLS group member. Distinct credentials are just distinct members — the identity-proof, invite, remove-all-leaves-of-a-pubkey, and admin-policy machinery in marmot-ts all operate per credential pubkey and already handle N members. This also matches GPT56-AUDIT §13.10's recommended identity model ("Option B: separate chat key per physical device").

1. Why the current design cannot do multi-device

Today's chat identity (packages/app/src/lib/chat/identity.ts:374-394):

So the shared-key model is simultaneously what blocks multi-device and the cause of the two nastiest chat-recovery bugs. Per-device keys dissolve both.

2. Library-level feasibility (vendored marmot-ts 0.6.0 / ts-mls 2.0.0-rc.14)

Facts verified in the vendored code:

3. Coordinator-level feasibility

Better than expected — the coordinator's data model is already multi-key-per-account:

What the coordinator is missing is exactly what PROTOCOL-NIP §10.2 adds: proof of possession in 21607 (today ChatHandoffCard.svelte:36-57 will attest any npub the user types — griefing/mis-binding becomes worse, not better, once devices multiply), plus a per-account device cap (MAX_CHAT_KEYS_PER_ACCOUNT = 10) and the roster chat_keys array.

4. Target design

Normative wire details in PROTOCOL-NIP.md §10. Summary of the moving parts:

  1. Every device mints its own chat keypair on first chat use — all account types. Local-nsec accounts stop chatting as their account key ([DECISION D3]; see §6 for the interop trade-off). The key lives only in that device's IndexedDB; it is never backed up, exported, or restored. client_id stays as the per-device 30443 slot id.
  2. Attestation with proof of possession (21607 v2): account-sealed {op, chat_pubkey, label, client_id, proof} where proof is a schnorr signature by the device key over a domain-separated digest. revoke per device key; full attendee revocation removes all of the account's leaves (unchanged).
  3. Roster carries chat_keys: [{pubkey, label?, added_at}] per attendee (replacing the never-populated singular chat_pubkey), so Nostrautica clients dedupe the member list by account, show "Alice (2 devices)", and can render per-device revoke UI for the user's own devices. Display-name resolution keeps the existing kind-0-by-sender-pubkey path (EventChat.svelte:241-264) as fallback — it is already per-device-agnostic.
  4. Device kind-0 on chat relays: each device key publishes a kind-0 (name = account display name) so external Marmot clients (White Noise) show humans instead of hex. This generalizes what the app already does for remote-signer chat keys (identity.ts:406-424 publishes "Nostrautica <name> (chat)" with the account npub in about, to the event relay set including the Whitenoise relays). Per resolved decision D4, v2 keeps the account npub in about (external clients can verify and group devices by account; the public device→account link is accepted) and scopes publication to the chat relay set only.
  5. Chat-device-key backup (31602 variant) retired. Nothing to restore ⇒ App-11 (fail-open restore) and App-12 (backup convergence) are deleted, not fixed. A "lost device" is handled by revoking its key from any other logged-in device (or by the fact that event chats are time-bounded anyway).
  6. Relays: chat traffic uses event relays ∪ the chat interop set. The ["chat_relay", …] 31600 tag anticipated here shipped on 2026-07-28 — the interop relays are no longer unioned into config.relays, because they reject every kind outside the Marmot/NIP-17 chat surface (see NIP §"Event Networking Config").
  7. Welcome/joining: unchanged per device — each device publishes its own 30443, the coordinator invites it, the Welcome arrives gift-wrapped to its 10050 relays, and the device joins with its own leaf. The unbound-candidate routing rule (NIP §10.4) applies per device.
  8. Multi-tab (H-7) is orthogonal and remains required. Two tabs in one browser profile are one device sharing one IndexedDB state store (chat/stores.ts:179-235) — per-device keys do not change that. Web Locks leader + BroadcastChannel proxy (or read-only secondary tabs as interim) ships alongside.

What a user experiences

5. Change list (from the verified assumption-site inventory)

App:

Protocol:

Coordinator:

6. Trade-offs and risks

7. Migration

Test users only ⇒ no compatibility path needed (flag day with wire v2):

  1. Ship protocol v2 schemas (21607 v2, roster chat_keys, backup schema retired).
  2. Coordinator: proof verification + cap + roster population. Existing groups keep working — current members were added under v1 rules; new adds require v2 attestations.
  3. App: mint-per-device identity; delete restore/backup paths; device UI; H-7 leader election.
  4. Existing attested keys: keep honoring stored bindings (they were first-binder-wins verified); optionally require re-attestation with proof on next session for hygiene.
  5. Delete stale 31602 chat-backup events best-effort (NIP-09) on first v2 session.