# Peers and groups Source: https://selfstore.dev/docs/peers Share a store between people over read-only links, and passwordless groups with per-member keys - built on the advanced store. Sharing usually means someone grants write access to their storage, then worries about it forever. selfstore's model removes that step: **nobody grants write access to anyone.** Peers are an advanced capability - reach them through `store.advanced` on a simple store, or on a [`createLocalStore`](https://selfstore.dev/docs/advanced) store directly. ## The model: crossed read-only links Each member publishes their own copy on their own storage and shares it **read-only**. Everyone attaches the others' links as peers. Every converge folds each peer's copy in as one more replica and republishes the merged state.
Three members each sole-write one copy on their own storage and read the others' copies read-only, forming crossed links with no shared write access. Alice sole writer of its copy Bob sole writer of its copy Carol sole writer of its copy read-only read-only read-only
No one writes to anyone else's storage. Each member is the sole writer of one file and reads the others; every converge folds them into the union, so each copy becomes a full backup of the group.
- **One writer per file**: no write races, by construction. - **Copies converge to the union**: every member is a full backup of the group. - **Gossip is transitive**: a star around one member propagates everything. ```ts import { webdavTarget } from 'selfstore/advanced'; // Bob's copy, read-only. Any BackupTarget is also a PeerSource; a bare public // link is three lines: { label:'Bob', load: () => fetch(url).then(r => r.ok ? r.blob() : null) } const bob = webdavTarget.peer({ url: BOB_SHARE_URL, label: 'Bob' }); const advanced = store.advanced; // the full store under the simple one advanced.attachPeer(bob, { id: 'bob' }); // read-only for (const p of advanced.state.peers) { if (p.lastError) console.warn(`${p.label}: ${p.lastError.code}`); // never blocks your saves } ``` Peers are held in memory, so re-attach them at boot (like `restoreTarget`). Per-peer trouble (unreachable, wrong passphrase, newer schema) lands on `state.peers[].lastError` and **never** gates the store: a friend's broken link must not break your app. ## Group crypto, option one: a shared passphrase Simplest: the group agrees on one passphrase out of band; every member's copy is encrypted with it. Fine for a household; rotating it means everyone re-keys. ## Option two: passwordless groups (per-member keys) No shared secret. Each member holds an Ed25519 + X25519 keypair, every published copy is signed by its author and sealed per recipient, and membership is an admin-signed manifest with rollback protection. The helpers live at `selfstore/groups`: > This one subpath is **experimental**: no application has shipped it yet, so > its API may change in a minor release. The file format it writes is not - see > [the reference](https://selfstore.dev/docs/api-peers#selfstoregroups). Take option one if you > cannot absorb a recompile. ```ts import { identityVault, signManifest, newGroupId } from 'selfstore/groups'; // Each member, once per device (sealed at rest under a device key): const identity = await identityVault(cache.kv).loadOrCreate(); // The ADMIN alone signs the membership manifest: const signed = await signManifest( { v: 1, group: newGroupId(), seq: 1, admin: identity.sigPub, members }, identity.sigPriv, ); // Every member attaches with { group } - no password parameter at all. The // STORE verifies the signed manifest against the admin key pinned from the // invite (TOFU); a bad signature rejects with SIGNATURE_INVALID first. await advanced.attachTarget(myTarget, { group: { identity, admin: pinnedAdminSigKey, manifest: signed }, }); ``` `state.peers[].author` is then a **verified** member id. Membership changes: the admin signs `seq + 1` and everyone applies it with `advanced.setGroup(next)` (re-verified against the pinned key; an older manifest throws `MANIFEST_ROLLBACK`). Removing a member seals the future only. Optionally lock the identity itself behind a passphrase (`identityVault(kv)` gains `protect`/`unlock`/`isProtected`), so a copied browser profile cannot use it. Needs WebCrypto Ed25519 + X25519 (evergreen browsers, Node 20+); check `groupCryptoAvailable()`. ## Where the full story lives The complete model, the failure table and the operational notes are in [PEERS.md](https://github.com/selfstoredev/selfstore/blob/main/PEERS.md); the on-disk format is section 12 of the [spec](https://github.com/selfstoredev/selfstore/blob/main/SPEC.md); the trust analysis is the [threat model](https://github.com/selfstoredev/selfstore/blob/main/THREAT-MODEL.md). Async sharing between a few people is the design point; live collaboration is not (embed a CRDT for that). Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt