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: “NotificationActorreceives merged update-streams from all activeProtocolActorinstances (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 singleRootSupervisor→SessionAuthActor,ProtocolActor,ChatSupervisor,MediaSupervisor,PredictorActor,NotificationActor. No account dimension anywhere; noAccountIdin 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 (grepaccount×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,notificationall exist, all single-account (git logup toed4d66e).
- severity: high
Суть / Substance
Section titled “Суть / Substance”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:
- Storage shape. One sanakirja instance per account (mirroring
ADR-025’s “separate sanakirja instance” precedent for secret chats),
or one instance with an
account_idkey prefix / column? The two have opposite migration, vacuum, search-index and encryption-key behaviour. With per-account instances,search-millineeds 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. - 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).
- The predictor. ADR-031 puts the predictor event log in the
metadata tier.
PREDICT.mdtrains 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. - Outbox.
get_outbox()with no account parameter means an offline message from account A can be flushed on account B’s connection — and sinceChatIdis 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. - 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,
AccountIdin 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-millineeds a multi-index fan-out and a merged ranking pass; N× file handles and N× page cache. - (B) Single instance,
account_idas the first key component, one globalAccountRegistry. 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
AccountIdinto every contract after the storage format ships is expensive (it touches keys, indexes, keys-derivation and the predictor).
Предлагаемое решение
Section titled “Предлагаемое решение”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:
AccountIdnewtype per ADR-015;ChatRepository/StorageEnginemethods takeAccountId(or the trait is instantiated per account — pick one, but pick one; the latter is less invasive and arguably better isolation since aStorageEngineinstance physically cannot reach another account’s store).- 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. - Multi-index search:
search-milliopens N indexes and merges results with per-chat-id namespacing; the merged result must carry(AccountId, ChatId, MessageId)becauseChatIdis only unique per account. This belongs in theSearchEnginecontract, which currently doesn’t exist at all (my finding 011). - Outbox: scope per account, and make the outbox flush path assert
(AccountId, ChatId)— never resolve aChatIdwithout its account. - 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. - Rewrite
TECHSPEC.md:418-425as 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 perREADME.md:41-45architecture goes in ADR.md.