eleven / messenger Public

docs: accounts, sign-in & recovery β€” the anchor model (#1049)

* docs: accounts, sign-in & recovery β€” the anchor model (successor to the passkey-only invariant)

Replaces the 'no email, no social login, ever' Day-1 invariant after it was
field-falsified by real lockouts (macOS ceremony can't reach non-iCloud
passkeys; 1Password PRF mismatch; one-rung locked card). New model: anchored
ownership, anonymous membership, and the identity-vs-keys split β€” with
account classes (owner / member / identifier-less / guardian-managed), the
recovery ladder, SiwA + synced-DEK design, blind-indexed email, paper
recovery kits, and the updated App Store 4.8 analysis. Design only; nothing
built.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: handoff (vault as the bus) + invite-link redesign (pretty now, PAKE-short later)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: adopt / as the invite-fragment separator (#Paul/<T>) β€” collision-free and reads like a path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell f8ad68960c6c899805bb0fce98ca11f9308aaeea parent 35df06c
2 files changed, +394 −22
  • docs/accounts-and-recovery.md +367 −0
  • docs/onboarding-and-home-server.md +27 −22
diff --git a/docs/accounts-and-recovery.md b/docs/accounts-and-recovery.md
new file mode 100644
index 0000000..92cb59d
--- /dev/null
+++ b/docs/accounts-and-recovery.md
@@ -0,0 +1,367 @@
+# πŸ€– Accounts, sign-in & recovery β€” the anchor model
+
+Status: **design, not built** (decided with Paul 2026-07-16; nothing here has
+shipped). This document supersedes the *Invariants* section of
+`onboarding-and-home-server.md` β€” the "no email, no social login, ever" line β€”
+after that invariant was field-falsified by real lockouts. Everything else in
+that doc (the welcome page, the home server machinery, the blob schema,
+enrolment, the fragility banner) remains accurate and in force; this doc is
+about **what identifies a person and what brings their keys back**.
+
+## Why we're revising a "permanent" decision
+
+Passkey-only sign-in was declared a Day-1 invariant. Within a week of the
+native apps leaning on it, two of our first users were locked out:
+
+- The macOS ceremony sheet only reaches iCloud Keychain (plus QR/security
+ keys). A passkey living in a Chrome profile or a third-party manager is
+ invisible to it β€” the user sees "a passkey window and no way to enter a
+ password."
+- 1Password's macOS provider returns a PRF that doesn't match its own browser
+ extension's for the same credential, so even a *present* passkey can fail to
+ decrypt (the browser-handoff workaround exists because of this).
+- The locked-space card offered exactly one rung ("Sign In with Eleven…") and
+ a destructive one ("Remove from This Mac"). One-rung ladders read as "the
+ app is broken", not "the design is working".
+
+The root cause is architectural, not cosmetic: the passkey was doing **two
+different jobs at once**, so any failure of the ceremony was total.
+
+## The two jobs
+
+Every mechanism in this design does one or both of:
+
+1. **Identity** β€” proving you're the same person who enrolled. Lets the home
+ server return your record and gates destructive operations.
+2. **Keys** β€” holding or deriving the client-side secret (the KEK) that
+ unwraps the vault **DEK**. The DEK decrypts the blob, which holds live
+ sessions + per-instance private keys. Nothing server-side can ever do this
+ job β€” that's the one rule.
+
+| Mechanism | Identity | Keys | Notes |
+|---|---|---|---|
+| Passkey + PRF | βœ“ | βœ“ | today's model; elegant, and why its failure is total |
+| Sign in with Apple | βœ“ | β€” | stable team-scoped `sub`; can request no email at all |
+| Email code | βœ“ | β€” | ecosystem-neutral; blind-indexed (below) |
+| iCloud-synced DEK item | β€” | βœ“ | invisible; Apple-ecosystem only |
+| Device key + approval | β€” | βœ“ | transfers keys to a new device, given identity |
+| Paper recovery kit | β€” | βœ“ | printed wrap of the DEK; survives everything |
+| Guardian wrap | β€” | βœ“ | DEK/credentials wrapped to another member's key |
+
+An **identity anchor alone can never decrypt anything.** That single sentence
+forbids the tempting shortcut (email-gated server escrow of the DEK) and
+shapes everything below.
+
+## The new invariants
+
+What "no email, no social login, ever" weakens to β€” each line deliberate:
+
+1. **Membership stays anonymous.** Joining a space requires no identifier,
+ ever β€” invites remain the only membership door. No change from today.
+2. **Ownership is anchored.** Every space's recovery chain must terminate at
+ a member whose recovery survives total device loss β€” normally the owner.
+ Practically: *creating* a space requires an anchor; *joining* one never
+ does. The owner's anchor is a **pair**: an identity anchor (email, Sign in
+ with Apple, or a successor β€” see the open slot) *plus* a key anchor (on
+ Apple platforms the synced DEK rides along invisibly; otherwise the paper
+ kit).
+3. **Providers attest; they are never the identity.** A third-party sign-in
+ is at most a verifier of an anchor. Our record is a blind index (a hash) β€”
+ there is no provider user-id column, no stored email address, and swapping
+ providers never orphans an account.
+4. **The server never holds a readable key.** No escrow of the DEK under any
+ server-side or identity-gated key, ever β€” a compromised home server plus a
+ passed email gate must still yield nothing. Recovery that "just works with
+ an email" is only achievable by breaking the one rule; we don't.
+5. **The home server stays strictly additive** (unchanged): no enrolment
+ record β†’ no requests to it, ever.
+6. **Copy stays human** (unchanged rule): never "passkey", "PRF", "key",
+ "escrow", or "backup" in UI. People sign in, save their spaces, and add
+ ways back in.
+
+## Account classes
+
+| Class | Identity (job 1) | Keys (job 2) | Lost everything |
+|---|---|---|---|
+| **Owner** (anchored) | SiwA / email / successor | synced DEK, or paper kit | full restore via anchor pair |
+| **Member, anchored** (optional) | same menu | same menu | same ladder as owner |
+| **Member, identifier-less** | the key *is* the identity | passkey, paper kit, or nothing | re-invite floor |
+| **Guardian-managed** | none β€” guardian vouches | wrapped to guardian's key | guardian re-links, instantly |
+
+- **Owner.** Whoever creates a space attaches the anchor pair at creation
+ (or is nudged until they do β€” open decision). This lands the identifier
+ exactly where accountability already lives: space creation is the abuse
+ surface (quotas, reaping, billing), membership never was (invite-gated).
+- **Identifier-less member.** Today's model, kept on purpose for
+ privacy-maximalists: bootstrap by invite, optionally strengthen with a
+ passkey or a paper kit, accept the re-invite floor. This class is why
+ the product can still truthfully say "no signup required".
+- **Guardian-managed** β€” the family case. A child has no email and no
+ passkey. The guardian's signed-in app mints the child's credentials
+ (an add-device link / QR β€” machinery that exists today) and keeps a copy
+ as a **managed entry in the guardian's own vault**. The child has *no home
+ account at all* β€” no server record, no identifier, nothing collected
+ (the right COPPA/GDPR-K posture by construction). Restore = guardian opens
+ their app β†’ managed member β†’ "link this device" QR. A printed **paper kit
+ per child** is optional redundancy for the guardian losing their own
+ account. Disclosed honestly: a guardian holding a member's keys can read
+ and act as that member β€” that is what guardianship *is*, and the UI says
+ so at creation. (A later variant can give an older child their own home
+ account with the DEK additionally wrapped to the guardian β€” the graduation
+ path β€” but the vault-entry form is the v1.)
+
+## What recovery restores β€” the ladder
+
+Top rung first; each rung only matters when everything above it is gone:
+
+1. **Any surviving signed-in device** β€” everything, silently (the cached DEK;
+ today's silent sync).
+2. **A synced key anchor** β€” a passkey that synced, or the iCloud-synced DEK:
+ new device + one identity ceremony = full restore, zero surviving devices
+ needed.
+3. **Approve from another device** β€” a new device signs in with an identity
+ anchor; an enrolled device gets a push ("Approve this Mac?"); on approval
+ the DEK is re-wrapped to the new device's ephemeral key. This is the
+ add-device flow wearing mainstream clothes (WhatsApp/Signal/Apple trained
+ everyone on it) and is the workhorse for web and non-Apple platforms.
+4. **The paper kit** β€” scan/upload, keys return. Survives ecosystems.
+5. **Control-only recovery + the re-invite floor** β€” identity anchor alone.
+ The owner recovers *control* of the space (see below) and can re-admit
+ anyone; a re-invited member gets the **same stable thread keys re-wrapped
+ to their new keypair by any live member's client, so full history
+ decrypts**. What this rung cannot do: if literally every member's key
+ material is gone (a solo space; a whole family's devices at once), the
+ history is cryptographically sealed. That is the design working β€” and the
+ reason invariant 2 requires the owner's anchor to be a *pair*, not an
+ email alone.
+
+## Sign in with Apple, precisely
+
+The seamless path for the current (Apple-heavy) user base:
+
+- **Request no email scope.** The stable team-scoped `sub` is the account
+ key, stored as `HMAC(pepper, sub)` β€” nothing mailable, nothing to leak.
+ Apple learns sign-in timing; we learn nothing new.
+- **Pair it with the synced DEK.** The native DEK cache is deliberately
+ device-local today (`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`,
+ `ios/LChatCore/.../Keychain.swift`). Storing the DEK *also* as a
+ synchronizable iCloud Keychain item makes the key travel invisibly β€” same
+ trust anchor as an iCloud passkey, zero WebAuthn ceremonies, and the
+ third-party-provider bug class (the 1Password PRF mismatch) disappears
+ because credential managers are never in the loop.
+- **Job 1 gates job 2.** A synced DEK alone would auto-unlock on any device
+ signed into the same Apple ID (family iPad on a parent's account). The
+ synced item is used only after a fresh SiwA assertion for the matching
+ hashed `sub`. Face ID on the SiwA sheet is the one visible moment.
+- **The honest trade, written down:** the "zero surviving devices" recovery
+ trust moves *into* Apple's ecosystem (Apple ID + iCloud Keychain escrow)
+ rather than being eliminated. That is exactly what the seamless experience
+ buys, and the identifier-less class is the opt-out.
+- **Web and non-Apple:** SiwA's web flow exists but presumes an Apple ID.
+ The web keeps passkey-PRF as its local mechanism and gains rung 3
+ (approve-from-phone). The email/successor anchor is the ecosystem-neutral
+ spine when it lands.
+
+## Handoff: onboarding that travels
+
+The goal (Paul, 2026-07-16): with identity at the *front* of onboarding, a
+person can start joining on the web and finish in the app β€” or the reverse β€”
+**without losing any fidelity**. Identity-first is what makes this nearly
+free:
+
+- **Today's order strands progress.** Claim-first-save-later means the
+ claimed credentials, chosen name, and onboarding position live in one
+ browser until the person explicitly enrols β€” the "Open in Eleven" jump has
+ to smuggle state across (the fragment parked in `sessionStorage`, the
+ enrolment popup dance).
+- **Identity-first inverts it.** Sign in is the first step (one Face ID via
+ SiwA on Apple devices), so every artifact of onboarding lands in the vault
+ *as it is created*: the claim writes the credential entry in the same
+ breath (the old doc's "golden path", now the default because sign-in got
+ cheap). The vault is the handoff bus.
+- **Fidelity is then free, not built.** Once keys + session cross via the
+ vault, the rest follows automatically: the onboarding conversation is
+ messages in a thread and mini-app state snapshots, both of which sync to
+ any device holding the keys. Jumping devices resumes the scripted
+ onboarding at the exact message β€” no state machine to serialize, no
+ "continue where you left off" feature to build. The jump itself carries
+ nothing sensitive: "Open in Eleven" / universal link β†’ app signs in with
+ the same identity (same Apple ID: zero ceremony) β†’ vault pull β†’ the space
+ and the conversation are already there.
+- **Reverse direction** (native β†’ web) is rung 3 of the ladder: the web
+ signs in with the identity anchor and an enrolled device approves it β€” or
+ a QR from the app, exactly the add-device mechanics.
+- **Identifier-less joiners** keep today's mechanics (fragment rides the
+ universal link; post-join nudge) β€” handoff fidelity is one of the things
+ an anchor *buys*, stated as such in the nudge copy.
+
+## Invite links: pretty, and how short they can get
+
+Today's link is `…/join/join#e1.<T>~<encoded name>~<brandingKey>` β€” and each
+ugly part exists for a different reason, so they improve on different
+schedules:
+
+- **`/join/join` β€” pure accident.** The path label slot is cosmetic
+ (`invite-redesign.md`) and every minter hardcodes it to `"join"`. Free to
+ fix: a single `/invite` path, doubling as the format version (old
+ `/join/join#e1.` links parse forever).
+- **`~<name>` β€” in the fragment on purpose.** Member names are E2EE; a name
+ in the *path* would hand it to the server (access logs included). So
+ `…/invite/Paul/<secret>` as a default breaks the rule. The name can lead
+ the fragment instead β€” `…/invite#Paul/<secret>` β€” which reads like a path
+ (the `#` is the only tell) and stays server-blind. `/` is the right
+ separator, not `.`: it's a legal, unescaped fragment character that
+ browsers/linkifiers/QRs carry verbatim, and `encodeURIComponent` escapes
+ `/` inside names (but leaves `.` alone), so the slots are collision-free
+ by construction β€” a raw `/` can only be our delimiter. (Name-in-path could
+ be offered as an explicit per-link "include my name in the address"
+ opt-in β€” the space name of an unsealed instance is already public
+ branding, so *that* variant leaks nothing β€” but the default stays blind.)
+- **`~<brandingKey>` β€” the price of sealed instances.** On a sealed instance
+ nothing is public, so the link must carry the key that unlocks the welcome
+ card. Unsealed instances don't need it. Unavoidable where it applies.
+- **`<T>` (22 chars) β€” load-bearing, for now.** T is not a ticket; it
+ offline-wraps the group key (`wrapped = AES-GCM(groupKey, HKDF(T))` sits in
+ the server's DB). A shorter T is brute-forceable *offline* by anyone who
+ steals the DB β€” a 10-char secret falls to a GPU farm in days β€” which would
+ break "compromise the server, get nothing". 128 bits (22 chars, re-encoded
+ base58 so it at least *looks* less like line noise) is the floor **while
+ invites carry the key offline**.
+- **Redirects can't shorten the secret.** Browsers do carry a fragment
+ through a redirect (that's how a short-*domain* bouncer could work), but a
+ server can only put in a `Location` what it knows β€” and it must never know
+ T. The secret travels in the link the person shares, full stop.
+
+So there are two roads:
+
+1. **Cosmetic redesign (no crypto change):**
+ `https://xyz.11ch.at/invite#Paul/2vXk9QmR4pTnWb8zJcHe6f` β€” one path word,
+ name up front, base58 secret, branding key as a third slot only where
+ sealed (`#Paul/<T>/<brandingKey>`). Roughly half today's length, and
+ honest about which half remains. Grammar notes: the name slot is always
+ present (falling back to the space name) so a new link is never
+ confusable with a legacy raw-T fragment; version detection is
+ fragment-contains-`/` β†’ this format, `e1.` prefix β†’ current, else legacy.
+2. **Genuinely short links = the parked PAKE phase** (`invite-redesign.md`
+ Phase 2). A low-entropy secret is only safe in a *live* exchange
+ (Magic-Wormhole style): the server relays an exchange it can't read, the
+ short code authenticates it against MITM, and the key travels
+ device-to-device β€” no offline-crackable blob, nothing a compromised
+ server can claim for itself. The historical blocker was needing a member
+ online during the join; **the sidecar bot dissolves it** β€” it's a member,
+ it holds keys, it's always on, so bot-attended spaces get instant short
+ joins (and unattended ones degrade to "waiting for Paul to let you in",
+ which is respectable UX in its own right). This is also where invite
+ links become *words* β€” `…/invite#Paul/coral-piano-42` β€” which is the
+ least scary a secret can look.
+
+## Email (and the successor slot)
+
+Email is one implementation of the terminal identity anchor β€” **the slot is
+deliberately abstract** because a stronger alternative is being designed
+(Paul, 2026-07-16; unspecified here). Whatever fills the slot must provide:
+a user-memorable claim ("this account is mine"), a reachable out-of-band
+channel to complete a challenge, and a blind-indexable identifier.
+
+If plain email fills it:
+
+- **Blind index:** store `HMAC(pepper, normalize(email))` only. At recovery
+ the user types their address; we hash, match, and send a code to what they
+ typed. No plaintext address is ever stored. Normalization is lowercase
+ only β€” plus-addressing and provider dot-games are respected, not stripped.
+- **Honest limits, for the copy and the threat model:** email hashes are
+ dictionary-reversible for anyone holding the pepper (tiny input space);
+ the running server and the mail provider both see the plaintext address
+ transiently at send time. The claim softens from "we can't spam what we
+ don't have" to *"we only see your address the moment you use it, and never
+ keep it."* Defensible; say it exactly like that.
+- **Fully native-able:** a 6-digit code works in a plain text field β€” no
+ WebAuthn sheet, no browser bounce, no provider roulette. The exact
+ complaint that started this ("no way to enter a password") dissolves.
+- **Costs:** outbound mail infrastructure (SES/Postmark, SPF/DKIM, bounces);
+ the provider becomes a party who sees addresses and timing; per-address
+ rate limits and disposable-domain policy become the abuse levers.
+
+## The paper recovery kit
+
+A one-page PDF, **generated client-side only** (browser or app β€” the server
+never sees the plaintext payload), in the tradition of 1Password's Emergency
+Kit and Signal's backup key:
+
+- **Owner kit:** the home credential id + the DEK wrapped under a printed
+ code (the same passphrase-wrap primitive the operator vault's recovery
+ escrow ships β€” `vault: recovery escrow`, #995–#997). Tiny; QR-encodable.
+- **Child kit:** the managed member's credential bundle (instance URL +
+ keypair) as a QR.
+- **Restore:** scan/upload β†’ keys return. For credential-bundle kits this
+ needs one small server feature β€” proving possession of the instance
+ keypair re-issues a session β€” specified when the kit is built, not here.
+- **Framing:** the paper is a house key and the UI says so ("Keep this
+ somewhere safe at home"). We never generate it server-side, never store a
+ copy, and never make it a sign-up speed bump β€” it's offered from settings
+ and at space creation, not demanded.
+
+## Control-plane recovery (why the owner can always re-admit)
+
+The admin token is authorisation, not decryption β€” its hash lives in the
+instance DB, and the cloud provisioner already knows which home account
+created which space. So "recover control" is a service-layer capability
+re-issue gated on the owner's identity anchor: verify the anchor, re-issue
+the admin link, and the owner is back to roster management β€” able to
+re-invite themselves and everyone else (rung 5). Self-hosters have the same
+power through `/data` on their own box. No key material is involved at any
+step, which is what makes this rung safe to gate on identity alone.
+
+## App Store guideline 4.8
+
+- An **own-account system** (email code, passkey) does not trigger 4.8 at
+ all β€” same exemption we rely on today.
+- **Sign in with Apple, offered alone, satisfies 4.8 by construction** β€” it
+ *is* the privacy-respecting option the rule demands β€” and future-proofs
+ the day a second provider (Google-as-attester) is ever added, because the
+ compliant option is already present.
+- The order therefore stays: SiwA and/or email first; other providers only
+ ever as additional attesters, never the primary.
+
+## Who sees what β€” the deltas
+
+| Party | Sees (new, per class) |
+|---|---|
+| Apple | that a SiwA user signs in, and when |
+| Mail provider | addresses + timing, at send time only (email class) |
+| Home server | blind indexes; one extra wrapped copy of the DEK per anchor |
+| A guardian | everything their managed member can see (disclosed at creation) |
+| Paper kit finder | whatever the kit unlocks β€” physical custody is the control |
+| A compromised home server | still nothing readable (invariant 4) |
+
+## Phasing
+
+0. **Sweep (no model change, this week):** the locked-space card gains its
+ missing rungs β€” "Link from your iPhone" (add-device QR) and "Ask for a
+ new invite" β€” and failure copy that never dead-ends at a ceremony sheet.
+ The **cosmetic invite-link redesign** (`/invite#Name/<T>`) also rides
+ here: no crypto change, old links parse forever.
+1. **SiwA + synced DEK** on iOS/Mac; web gains approve-from-phone.
+ Identity-first onboarding lands with it β€” the vault becomes the handoff
+ bus and web↔native jumps stop losing fidelity.
+2. **Guardian-managed members + paper kits** (the family story).
+3. **Owner anchoring at creation** β€” email or the successor anchor; the
+ provisioner starts requiring/nudging it.
+4. **Providers-as-attesters**, if ever. **Short/word-code invites** (the
+ PAKE phase, bot-attended) slot in whenever they earn their build.
+
+## Open decisions
+
+- **The successor identity anchor** (Paul's design) β€” this doc holds the
+ slot; email is the fallback filler.
+- Anchor **required** at space creation vs **nudged** until attached.
+- Synced DEK: default-on for SiwA accounts vs opt-in ("Keep me signed in on
+ my other devices").
+- What identity-only recovery may show before a key anchor confirms:
+ recommendation **nothing** β€” the space list is metadata worth the same
+ protection, so rungs 3–5 reveal it only after approval/kit/re-invite.
+- Invite links: whether "include my name in the address" (name-in-path)
+ is offered as an explicit opt-in, or names stay fragment-only always.
+- When the PAKE short-invite phase earns its build β€” and whether the
+ sidecar bot is a *sufficient* counterparty for unattended spaces or the
+ flow always needs a human approval option too.
diff --git a/docs/onboarding-and-home-server.md b/docs/onboarding-and-home-server.md
index 33f895c..aa2ebca 100644
--- a/docs/onboarding-and-home-server.md
+++ b/docs/onboarding-and-home-server.md
@@ -1,12 +1,18 @@
# πŸ€– Onboarding & the home server β€” design
-Status: **shipping, not proposed.** Passkey-only sign-in is a Day-1 invariant β€”
-no passwords, no email, no social/third-party login, and nothing on the roadmap
-changes that (see Invariants below). The home server, enrolment, and the
-signed-out-root restore flow are built (see the inline *Implemented* markers);
-silent multi-device sync (phase 3c) is the main piece still landing. This doc is
-the single place for where we landed. Builds on `invite-redesign.md` (invite
-secret `T` in the URL fragment, server-blind claim).
+Status: **shipping, not proposed** β€” for the machinery. The home server,
+enrolment, and the signed-out-root restore flow are built (see the inline
+*Implemented* markers); silent multi-device sync (phase 3c) is the main piece
+still landing. This doc is the single place for how that machinery works.
+Builds on `invite-redesign.md` (invite secret `T` in the URL fragment,
+server-blind claim).
+
+> **The identity invariants were revised 2026-07-16.** "Passkey-only sign-in,
+> no email, no social login, ever" was field-falsified by real lockouts within
+> a week of the native apps leaning on it. The successor design β€” anchored
+> ownership, anonymous membership, and the identity-vs-keys split β€” is
+> **`accounts-and-recovery.md`**, which supersedes the Invariants section
+> below. The invariants that survive unchanged are restated there.
## Goals
@@ -21,21 +27,18 @@ credentials behind it) should be restorable on a fresh device and kept in sync
across devices. All without usernames, passwords, or weakening the one rule:
**a compromised server yields nothing**.
-## Invariants (non-negotiable)
-
-- **No accounts.** Identity stays per-instance keypairs; membership only ever
- enters via invites. No federation.
-- **No social or third-party login, ever.** Sign-in is a passkey (or an invite
- link) β€” identity is never brokered by Facebook / Google / Apple / X / etc.
- This is a permanent product decision, not a gap we'll fill later. Beyond
- keeping the model accountless, it has a concrete payoff for the native app:
- App Store guideline 4.8 (the "you must also offer an equivalent login option"
- rule, i.e. Sign in with Apple and friends) is triggered *only* by apps that
- use a third-party or social login service. A passkey-only app is explicitly
- exempt β€” *"Your app exclusively uses your company's own account setup and
- sign-in systems"* β€” and requires no email address, so there is nothing for an
- "equivalent" login to be equivalent *to*. Adding a social login would flip
- that requirement on; we never will.
+## Invariants (revised β€” see `accounts-and-recovery.md`)
+
+> **Superseded 2026-07-16.** This section originally declared "No social or
+> third-party login, ever" and "no email" as permanent decisions. The
+> replacement invariants β€” membership stays anonymous, ownership is anchored,
+> providers attest but are never the identity, the server never holds a
+> readable key β€” live in **`accounts-and-recovery.md`** together with the
+> rationale (including the updated App Store guideline 4.8 analysis). The
+> invariants below survive unchanged and remain non-negotiable:
+
+- **Membership enters via invites.** Identity stays per-instance keypairs;
+ no federation. (Joining a space still requires no identifier, ever.)
- **The home server is strictly additive.** A client with no enrolment record
makes **no requests to any home server, ever** β€” no passive pings, no
"check for a backup" lookups. Enrolment is created only by an explicit user
@@ -145,6 +148,8 @@ concurrent reorder β€” acceptable until per-entry ordering earns a CRDT.
add-device links remain the universal fallback. Losing **all** passkeys means
the blob is cryptographically gone; that's the design working. Recovery
paths are your other enrolled devices, add-device links, or fresh invites.
+ (The full recovery ladder β€” including the non-passkey key anchors that
+ soften this β€” is `accounts-and-recovery.md Β§ What recovery restores`.)
### API sketch