Best Practices
Patterns for building reliable Marmot applications. They follow from how the engine handles convergence, commit lifecycle, and key material.
Keep key packages replenished
Others can only add you to a group if you have an unused key package published. Key packages are single-use (unless marked last-resort), so monitor your inventory and top it up:
for await (const packages of client.keyPackages.watchKeyPackages()) {
if (packages.length < 3) {
await client.keyPackages.create({ relays: myInboxRelays });
}
}Rotate periodically with client.keyPackages.rotate(ref, options) for post-compromise hygiene, and publish to the relays where invitations will look for them (your kind 10050 inbox relays).
Let convergence settle before relying on state
Outbound sends are convergence-gated: an intent submitted while group.convergenceStatus is not Settled is queued and flushed once concurrent commits resolve. Don't fight this with retries — submit the intent and let the engine order it. When you need to read settled state (e.g. before showing the member list as authoritative), check convergenceStatus === "Settled".
Treat ingest dispositions exhaustively
group.ingest yields a disposition per event. Handle the non-processed cases instead of assuming success:
deferred— not malformed; retry when more protocol state arrives.unreadable— terminal; the event can't be decrypted (e.g. a pruned epoch). Log and drop.removed— an inbound commit removed this member. Stop sending; decide when todestroythe tombstone.
Drive the UI from history, not from ingest
Render chat from group.history.subscribe(...), which delivers both self-sent and ingested messages through one stream. Listening to the applicationMessage event works too, but the history subscription gives you the full timeline, survives reloads, and is idempotent across relay backfill. Avoid building a separate "optimistic echo" path — the session already records self-sent rumors.
Persist before you publish
The engine uses publish-before-apply for commits: a commit is published, then confirmed or rolled back. Let the client manage this — use client.groups.commit / send rather than manually advancing state — so a failed publish doesn't leave your local state ahead of the group.
Isolate storage per account
Every account must use completely separate groupStateStore, keyPackageStore, and inviteStore instances. Key package stores hold private keys; sharing a backend across accounts leaks key material. Namespace stores by pubkey and rebuild the client on account switch — see Multi-Account Support.
Choose relays deliberately
- Publish key packages and receive gift wraps on your inbox relays (kind 10050);
getUserInboxRelaysresolves a peer's. - A group carries its own relay set in
group.groupData.relays; publish and subscribe to group traffic there, and update it withproposeUpdateMetadata({ relays }). - Use several relays for redundancy — Marmot's privacy and availability come from relay diversity, not from any single relay.
Clean up subscriptions
watch(), watchKeyPackages(), watchUnread(), and history.subscribe() are long-lived async generators. Break out of their loops (or abort them) when a component unmounts or the user switches accounts, and call group.dispose() when unloading a group, to avoid leaking listeners and timers.
Supply an account-identity-proof signer for interop
To interoperate with darkmatter and other Marmot v2 implementations, provide an accountProofSigner so your LeafNodes carry a valid marmot.account-identity-proof.v1. Invites from peers that validate proofs (and proposeInviteUser) will otherwise reject your key packages.
Next steps
- MarmotClient — lifecycle, managers, multi-account
- Proposals — commits, membership, metadata
- Architecture — the convergence and lifecycle model these practices follow