Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

Multi-account is specified in one section and contradicted by every contract it depends on

Источник: dev/fera-research/docs/oc1/007-multiaccount-specified-once-contradicted-everywhere.md (read-only; сгенерировано scripts/sync-docs.mjs)

Multi-account is specified in one section and contradicted by every contract it depends on

Section titled “Multi-account is specified in one section and contradicted by every contract it depends on”
  • kind: problem
  • area: modules / storage / protocol
  • evidence:
    • /home/nikki/dev/fera/project-design/context/TECHSPEC.md:418-425 — the only statement of multi-account in the whole corpus: “NotificationActor receives merged update-streams from all active ProtocolActor instances (one per account). Owns global mute/batching/priority policy… policy decisions … are evaluated globally across accounts, not per-account in isolation.”
    • Contradicted by the actor tree, TECHSPEC.md:102-113: a single RootSupervisor → SessionAuthActor, ProtocolActor, ChatSupervisor, MediaSupervisor, PredictorActor, NotificationActor. No account dimension anywhere; no AccountId in the tree.
    • Contradicted by the storage contract, TECHSPEC.md:136-143: get_messages(chat_id, from, limit), put_messages(chat_id, …), get_outbox() — no account parameter anywhere. Same for the protocol contract, TECHSPEC.md:55-62 (ChatRepository).
    • ADR-031’s field audit (ADR.md:648-664) lists no account discriminator in any field of the metadata or content tier.
    • ADR.md:349-352 (ADR-011) reasons about read-state purely per chat, with no account dimension; ADR-019’s predictor log (ADR.md:734-736) likewise.
    • Decisive evidence that this was never designed: conversation.md (2821 lines, the raw design session) contains zero hits for any per-account storage/DB/instance question (grep account × per.account|separate|db|database|instance → no results).
    • OQ-021 (runtime on-disk layout, OQ.md:608-649, “canonical layout above is FINAL”) fixes a single data root — it never accounts for N accounts.
    • Real code: ~/dev/fera/fera/core/crates — protocol, domain, storage, search, predictor, notification all exist, all single-account (git log up to ed4d66e).
  • severity: high

TECHSPEC.md §8 is the only place multi-account exists in the design corpus, and every contract it would have to be built on is single-account. This is a “one paragraph vs the whole rest of the spec” gap, and it is not a cosmetic one — each of the following is undecidable without an account model:

  1. Storage shape. One sanakirja instance per account (mirroring ADR-025’s “separate sanakirja instance” precedent for secret chats), or one instance with an account_id key prefix / column? The two have opposite migration, vacuum, search-index and encryption-key behaviour. With per-account instances, search-milli needs N indexes and search becomes a fan-out; with a shared instance, every key, index and query needs an account prefix, and a bug anywhere leaks one account’s messages into another’s search results — the worst possible bug class in a messenger.
  2. Encryption keys. ADR-016 derives content-tier keys from the user passcode (single-user assumption). Two accounts on one machine means either one passcode unlocking everything (bad) or per-account key material with per-account unlock state (unowned work).
  3. The predictor. ADR-031 puts the predictor event log in the metadata tier. PREDICT.md trains per-user models (ADR.md:742-743, “trained per-user”). Whose “user”? Account, machine, or install? Chat co-occurrence histograms across two accounts of the same person are a privacy leak of a different shape than within one.
  4. Outbox. get_outbox() with no account parameter means an offline message from account A can be flushed on account B’s connection — and since ChatId is not globally unique across accounts, the target can resolve to the wrong chat. This is a mis-delivery bug, i.e. sending private content to the wrong conversation.
  5. Notifications. §8’s own rule (“policy evaluated globally across accounts”) requires a global mute state that is not per account — but users expect per-chat/per-account mutes. Whether a mute in account A can suppress a notification from account B is exactly the kind of question that gets answered by accident.

There is no AccountId type anywhere in the corpus — the type-driven approach (ADR-015) that the project prides itself on has no account type, while every other core identifier (MessageId, ChatId, ReadState) has one.

Варианты решения (tradeoffs)

Section titled “Варианты решения (tradeoffs)”
  • (A) One sanakirja instance per account, AccountId in every key path. Strongest isolation (a bug in one account’s namespace cannot leak into another), natural per-account key derivation and natural “log out and delete” semantics. Costs: N instances to open/vacuum/upgrade; search-milli needs a multi-index fan-out and a merged ranking pass; N× file handles and N× page cache.
  • (B) Single instance, account_id as the first key component, one global AccountRegistry. One migration path, one index (with account as a filter/facet), simplest ops. Costs: catastrophic-bug blast radius is “all accounts”; deleting an account requires careful range deletion; encrypted fields still need a per-account key if accounts should be separately unlockable.
  • (C) Single instance, one key, per-chat-row account column. Reject — the “forgot the account predicate” bug is trivially easy here and the consequence is cross-account data exposure.
  • (D) Defer: declare single-account for the first release, say so explicitly in TECHSPEC §8 with a v1.5 ADR. Cheapest, removes the contradiction, but Telegram users routinely have 2–3 accounts, so it is a visible product gap; and retrofitting AccountId into every contract after the storage format ships is expensive (it touches keys, indexes, keys-derivation and the predictor).

Decide now, because the storage format and the key-derivation design are being written right now (the real repo already has core/crates/storage + search in flight, commits 004540f/ed4d66e).

Recommendation: (A) per-account sanakirja instance + AccountId as a first-class newtype in every contract, with these specifics:

  1. AccountId newtype per ADR-015; ChatRepository/StorageEngine methods take AccountId (or the trait is instantiated per account — pick one, but pick one; the latter is less invasive and arguably better isolation since a StorageEngine instance physically cannot reach another account’s store).
  2. Per-account Argon2id context strings ("fera-content-a1", "fera-content-a2", …) exactly as ADR-025 already does for "fera-secret-v1" — the precedent and the mechanism both exist.
  3. Multi-index search: search-milli opens N indexes and merges results with per-chat-id namespacing; the merged result must carry (AccountId, ChatId, MessageId) because ChatId is only unique per account. This belongs in the SearchEngine contract, which currently doesn’t exist at all (my finding 011).
  4. Outbox: scope per account, and make the outbox flush path assert (AccountId, ChatId) — never resolve a ChatId without its account.
  5. Predictor: pick one and write it down. Recommend per-account models (they are small, PREDICT.md §6 trains per-user on-device models), and keep the event log in the account’s own instance so logout wipes it.
  6. Rewrite TECHSPEC.md:418-425 as a section that owns the account model (instance-per-account, keys, search, outbox, notification policy), not just the notification fan-in. Add an ADR (032) — this is architecture, and per README.md:41-45 architecture goes in ADR.md.