eleven / messenger Public
π€ Accounts, sign-in & recovery β the anchor model
Status: plan of record β phase 1 largely shipped (decided with Paul 2026-07-16; status refreshed 2026-07-22). Live today: the six sign-in doors on home (passkey, email code, Apple, Google, Microsoft, GitHub β the #1112 card), identity anchors + ceremony proofs (stateless.go), the Sign-in options management step, account combine/merge (#1098), sealed anchor sessions, and the native passkey/Apple/email doors. Merged but dark: the synced DEK (#1144, flag off pending the device matrix). Not built yet: the /invite link redesign (the phase-0 locked-card rungs shipped on the Mac 2026-07-22; native add-device mint/QR still open), owner anchoring at creation, approve-from-another-device, the recovery-passkey offer, and the native OIDC doors (server legs done, client work open) β see Phasing below, per-item. (The paper recovery kit that used to sit in this list was cut on 2026-07-25 β Paul: recovery is a passkey, never a passphrase or a printed code; the recovery-passkey section below replaces it.) 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:
- Identity β proving you're the same person who enrolled. Lets the home server return your record and gates destructive operations.
- 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 | | Recovery passkey (manager / hardware key) | β | β | a second, independent credential; ecosystem-free | | 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:
- Membership stays anonymous. Joining a space requires no identifier, ever β invites remain the only membership door. No change from today.
- 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 a recovery passkey in a synced manager or on a hardware key).
- 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.
- 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.
- The home server stays strictly additive (unchanged): no enrolment record β no requests to it, ever.
- 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 recovery passkey | 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(s) 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 several), 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. The guardian's own recovery ladder is the redundancy: recover the guardian account and the managed entries come back with the vault. 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:
- Any surviving signed-in device β everything, silently (the cached DEK; today's silent sync).
- A synced key anchor β a passkey that synced, or the iCloud-synced DEK: new device + one identity ceremony = full restore, zero surviving devices needed.
- 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.
- A recovery passkey outside the ecosystem β a passkey in a cross-platform manager or on a hardware security key, enrolled as one more way in. This is what survives losing a whole vendor account: it is deliberately not parked where rung 2 lives.
- 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
subis the account key, stored asHMAC(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. Built (issue #1089):ios/LChatCore/.../DEKSync.swiftβ a second item beside the device-local one (servicecom.lchat.client.dek-synced, added by addition, never delete-then-add), write-gated on the account holding a SiwA anchor. Build-flag gated, OFF by default until the device matrix passes. - 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. In the built sliceDEKSync.releaseis the item's only reader, called solely with the token a fresh SiwA verify just returned; release further requires the resolved account's anchor list to contain the very anchor the item was written under AND the DEK to actually open that account's blob. - 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/invitepath, 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, andencodeURIComponentescapes/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
Locationwhat it knows β and it must never know T. The secret travels in the link the person shares, full stop.
So there are two roads:
- 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. - Genuinely short links = the parked PAKE phase (
invite-redesign.mdPhase 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 recovery passkey
Decision (Paul, 2026-07-25): recovery is a passkey, never a passphrase or a printed code. The earlier paper-kit design (a client-generated PDF wrapping the DEK under a printed code, riding the vault's passphrase-wrap primitive #995β#997) is dead β that primitive was removed from the product the same day. One kind of key everywhere; nothing to type, transcribe, or find in a drawer.
- Mechanically it's just another enrolment. Home already wraps the DEK per enrolled credential, so "add a recovery passkey" is the existing add-a-passkey ceremony pointed at a different home for the credential: a cross-platform password manager (1Password, Bitwarden, Google Password Manager) or a hardware security key. No new crypto, no new server surface.
- What paper used to buy β surviving a lost vendor account β a hardware key or cross-platform manager buys instead. The offer's copy steers there: a second passkey parked in the same iCloud account as rung 2 adds nearly nothing; one on a YubiKey in a drawer, or in a manager that syncs everywhere, is the real ecosystem escape hatch.
- Losing it is survivable and revocable. A found hardware key is inert without its PIN, and the credential (plus its DEK wrap) can be removed from the account like any other β unlike a printed code, which could never be un-printed.
- Framing: never a sign-up speed bump β offered from settings and at space creation ("add another way back in"), in the same no-jargon voice as the rest (invariant 6: the word "passkey" stays out of the UI).
- Managed members need no kit of their own: their credentials live as entries in the guardian's vault (above), and the guardian's ladder is the redundancy.
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) | | A found hardware key | nothing without its PIN β and the credential is revocable from the account | | A compromised home server | still nothing readable (invariant 4) |
Phasing
Status per item as of 2026-07-22:
- Sweep (no model change): 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. β partially built (2026-07-22): the Mac locked pane now carries both rungs (each opens the Add-from-link sheet with contextual guidance β the consume side of add-device links already worked) plus signed-in-but-still-locked feedback. Still open: a native mint/QR for add-device links (minting is web-only today), and no/inviteroute exists. - 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. β partially shipped: SiwA is live on web + both natives; the synced DEK is merged behind an off flag (#1144, device matrix owed); approve-from-phone and the identity-first ordering are not built. The web doors overshot the plan β Google/Microsoft/GitHub ship too; natively their server legs (
elevenmessenger://home-oidc) are done and the client halves are open. - Guardian-managed members + the recovery-passkey offer (the family story; settings + creation). β not built.
- Owner anchoring at creation β email or the successor anchor; the provisioner starts requiring/nudging it. β not built (creation from home is effectively anchored via the home session; a bare setup link still claims anonymously).
- Providers-as-attesters, if ever. Short/word-code invites (the PAKE phase, bot-attended) slot in whenever they earn their build. β not built.
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/passkey/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.