Peers and groups
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
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.
- 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.
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. Take option one if you cannot absorb a recompile.
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; the on-disk format is section 12 of the spec; the trust analysis is the threat model. Async sharing between a few people is the design point; live collaboration is not (embed a CRDT for that).