Search selfstore
v1.8.21

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.

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.
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).