# API: groups and households Source: https://selfstore.dev/docs/api-peers Complete reference for selfstore/groups and selfstore/households - identity vaults, signed manifests, the ShareBackend contract, and the household group that turns crossed read-only links into shared data. The two entry points behind sharing between **people** (as opposed to a user's own devices). The model and its threat analysis are on [peers and groups](https://selfstore.dev/docs/peers); this page is the surface. Most apps never import these directly. They surface through the [share](https://selfstore.dev/docs/widget-share) and [join](https://selfstore.dev/docs/widget-join) widgets, which drive a `ShareEngine` and a `JoinEngine` your app builds - often on top of what is here. ## selfstore/groups Passwordless groups: each member holds a keypair, an admin signs a manifest, and the backup's envelope carries one key slot per member. > **Experimental, and the only entry that is.** These exports may change shape, > or be withdrawn, in a **minor** release; every other subpath waits for a > major. The reason is evidence rather than doubt about the cryptography: no > application has shipped this API, so its shape has never been tested by a > second pair of hands, and it is the most security-sensitive surface in the > package. The **file** is not experimental - group mode is format generation 2, > [specified](https://selfstore.dev/docs/format) with a canonical test vector, and keeps the > guarantee every backup gets. What is at risk is a recompile, never anyone's > data. The shared-passphrase mode is unaffected. | Export | Signature | Notes | | --- | --- | --- | | `generateIdentity` | `() => Promise` | A fresh member keypair | | `publicIdentity` | `(identity) => ...` | The shareable half | | `keyId` | `(...) => string` | Stable id for a key | | `newGroupId` | `() => string` | | | `signManifest` | `(...) => Promise` | Admin-side | | `openManifest` | `(...) => Promise` | Verifies before returning | | `groupCryptoAvailable` | `() => boolean` | Feature detection | | `GROUP_KEYING` | constant | The keying scheme identifier | | `identityVault` | `(kv: KV) => IdentityVault` | Where a member's private key lives | ### IdentityVault ```ts interface IdentityVault { load(): Promise; save(identity: GroupIdentity): Promise; loadOrCreate(): Promise; clear(): Promise; isProtected(): Promise; unlock(passphrase: string): Promise; protect(passphrase: string): Promise; unprotect(passphrase: string): Promise; } ``` `loadOrCreate()` is the call an app makes at boot. `protect()` puts the private key behind a passphrase, so a copied browser profile does not carry group membership with it. **The store verifies manifests itself** - signature, member-key shapes, group binding - so group security never depends on an app remembering to call `openManifest`. That is a deliberate design choice: a security check an integrator can forget is not a security check. ## selfstore/households A household group: several people, each with their own backup file, reading each other's through crossed read-only links. ```ts function createHouseholdGroup(deps: { store: LocalStore; kv: KV; backend: ShareBackend; storageKey?: string; wallet?: () => Promise; }): HouseholdGroup; const HOUSEHOLD_GROUP_KEY = 'selfstore:households:group:v1'; ``` ### HouseholdGroup | Method | Signature | What it does | | --- | --- | --- | | `startShare` | `() => Promise<{ fileId, key }>` | Begin sharing; returns the invite capability | | `invite` | `() => Promise<{ fileId, key }>` | Another invite for the same group | | `openIncoming` | `(fileId, key) => Promise` | Read an invite without joining | | `join` | `() => Promise<'joined' \| 'no-invite' \| 'mismatch' \| 'error'>` | | | `syncGroup` | `() => Promise` | Converge the roster | | `leave` | `(walletFileId?) => Promise` | | | `restore` | `() => Promise` | Re-attach after a reload | | `state` | `HouseholdGroupState` (readonly) | | `join()` returns the same three named outcomes the [join flow](https://selfstore.dev/docs/api-flows) exposes: `'mismatch'` (this device already follows another share) and `'no-invite'` (spent, or meant for someone else) are expected answers, not errors. ### HouseholdGroupState ```ts interface HouseholdGroupState { active: boolean; isAdmin: boolean; memberCount: number; members: { fileId: string; label: string }[]; selfFileId: string | null; walletFileId: string | null; inviteCapability: { fileId: string; key: string } | null; memberships: MembershipInfo[]; } interface MembershipInfo { walletFileId: string | null; isAdmin: boolean; selfFileId: string; sharedBy: string | null; memberCount: number; announcePending: boolean; stale: boolean; } ``` ### ShareBackend: what you implement The group knows nothing about your storage provider. Everything provider-shaped goes through this contract: | Method | Signature | | --- | --- | | `createCopy` | `(existingFileId?, shareLabel?) => Promise` | | `copyTarget` | `(fileId: string) => BackupTarget` | | `dropCopy` | `(fileId: string) => Promise` | | `publishBulletin` | `(key, payload: SharePayload) => Promise<{ fileId, key }>` | | `revokeBulletin` | `() => Promise` | | `openIncoming` | `(fileId, key) => Promise` | | `takeStashedIncoming` | `() => Promise<{ key, fileId, content } \| null>` | | `rereadJoined` | `(fileId, key) => Promise` | | `announce` | `(mailboxId, copy: CopyLink) => Promise` | | `takeAnnounces` | `(mailboxId) => Promise` | | `peerSource` | `(link: CopyLink) => PeerSource` | `rereadJoined` returning the literal `'unreadable'` rather than `null` is the distinction that matters: a bulletin that is gone and one your key no longer opens call for different messages - the first is over, the second may be a revocation you should report. #### Building it over Google Drive Most of the file work is already in [`driveTarget`](https://selfstore.dev/docs/api-advanced#sharing-over-drive-companion-files), so a Drive backend is mostly wiring rather than REST: | Port method | What it becomes | | --- | --- | | `createCopy` | `driveTarget.createCompanion` + `driveTarget.share`, then `driveTarget.owner` for the label | | `copyTarget` | `driveTarget.secondary({ auth, kv }, fileId)` - **not** `preview`, whose `disconnect()` ends the primary Drive connection | | `dropCopy` | `driveTarget.unshare` then `driveTarget.deleteBackup`, in that order | | `publishBulletin` / `revokeBulletin` | a companion of its own, written with `exportSnapshot` under the link key | What stays yours is the part that needs a server, and it is worth naming before you start: `openIncoming`, `rereadJoined` and `peerSource` all **read another member's file**, which the `drive.file` scope cannot do. `announce` and `takeAnnounces` need a channel that outlives a tab. Both take a relay you host, or a gesture per file through the Google Picker. selfstore ships neither on purpose - it would be a server in a library whose whole claim is that there isn't one. ### Codes on the wire Invites travel as strings. These encode and decode them, and refuse malformed or future-version input loudly: | Export | Signature | | --- | --- | | `encodeShare` / `decodeShare` | `(payload: SharePayload) => string` / `(code: string) => SharePayload` | | `encodeAnnounce` / `decodeAnnounce` | same, for `AnnouncePayload` | | `toCopyLink` | `(v: unknown) => CopyLink` | | `toRoster` | `(v: unknown) => CopyLink[]` | | `randomId` | `() => string` | ```ts class HouseholdCodeError extends Error { readonly code: HouseholdCodeErrorCode; // 'malformed' | 'unsupported-version' } ``` `toCopyLink` and `toRoster` take `unknown` on purpose: a code comes off a URL a stranger produced, so it is parsed and validated, never cast. ```ts type CopyLink = DriveCopyLink; interface DriveCopyLink { provider: 'drive'; fileId: string; ownerEmail?: string; ownerName?: string; } interface SharePayload { v: 1; mailboxId: string; roster: CopyLink[] } interface AnnouncePayload { v: 1; copy: CopyLink } interface IncomingShare { projection: Record; share: SharePayload } ``` The `v: 1` field is why `decodeShare` can fail with `'unsupported-version'` rather than misreading a newer code: the version is checked before the shape is trusted. Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt