Domain model¶
Status: proposal v0.4. Types are conceptual, not a frozen SQL schema or ABI. Protocol · Architecture.
Versión en español: es/DOMAIN_MODEL.md
1. Vocabulary and ownership¶
| Entity | Identifier and essential data | Authority / location |
|---|---|---|
| Identity | identity_id = hash(domain, version, root_public_key); public root |
Root generated by the person; secret on client/kit |
| Device | Random ID, separate public keys, state | Identity authorizes; client keeps secrets |
| DeviceCredential | Root, device ID, keys, usage, validity, root signature | Client verification; public copy in the directory |
| DeviceManifest | Root, version, previous hash, active/revoked credentials, signature | Root; known maximums on each client |
| Realm | ID bound to the signing key; current Noise key; operational policy | Operator; does not authenticate people |
| RealmEndpointList | Sequence, realm Noise key, endpoints with type, priority and expiry, realm signature | Operator publishes; each client keeps its known maximum |
| RealmMembership | Realm, identity, role, state and quota | Server authorizes resource usage |
| Invite | Hash of a random token, expiry, remaining uses, permitted role | Realm, atomic consumption |
| KeyPackageRecord | MLS reference, standard bytes, device, expiry, state | Client generates/signs; relay distributes |
| Mailbox | Random ID, owning device, limits and generation | Recipient manages; server knows the owner |
| DeliveryCapability | Random secret, mailbox, scope, expiry and generation | Recipient shares; server verifies the hash |
| Envelope | Mailbox, random ID local to that delivery, ciphertext, expiration | Relay stores bytes; client interprets |
| Blob | Random ID, ciphertext, stored size, state and TTL | Client encrypts; relay retains temporarily |
| Conversation | Local/app ID, MLS group, title, policy, roster of people/devices | Clients and E2EE messages only |
| GroupState | MLS ID, epoch, transcript/context, own leaf and active secrets | MLS library in encrypted storage |
| MessageEvent | Random ID, author device, type, body, references | Client; validated against MLS authentication |
| RouteBundle | Device ID, relay by signing key, mailbox, capability, outer key, version, signature | Published by the recipient inside an authenticated exchange; no URLs, which come from the RealmEndpointList |
| RecoveryKit | Encrypted private root, minimal recovery metadata | User custody; distinct from the server backup |
| HistoryArchive | Exported records and optional attachments, version and manifest | Encrypted archive; no active MLS states |
Identity hashes have domain separation and an exact versioned representation. Do not use ambiguous concatenations or usernames as security keys. IDs and capabilities use a CSPRNG; proposal: 256 bits for capabilities and 128 bits or more for non-secret IDs. Knowing an ID does not grant access.
2. Relationships¶
erDiagram
IDENTITY ||--o{ DEVICE : authorizes
IDENTITY ||--o{ REALM_MEMBERSHIP : participates
REALM ||--o{ REALM_MEMBERSHIP : admits
DEVICE ||--o{ MAILBOX : receives
MAILBOX ||--o{ ENVELOPE : contains
DEVICE ||--o{ KEY_PACKAGE : publishes
CONVERSATION ||--o{ GROUP_LEAF : includes
DEVICE ||--o{ GROUP_LEAF : represents
CONVERSATION ||--o{ MESSAGE_EVENT : keeps_locally
The diagram combines local and server entities. CONVERSATION, GROUP_LEAF and MESSAGE_EVENT are not realm tables. The same person can belong to several realms with the same identity; that eases continuity and allows correlation across them. Using different identities is allowed if the user wants to separate contexts; they are not merged automatically.
3. Key lifecycle¶
| Material | Usage and residence | Rotation / loss |
|---|---|---|
| Ed25519 root | Signs credentials and manifests; stored encrypted, explicit unlock | Compromise requires a new root and re-verification; without it there is no strong continuity |
| Per-device MLS signing key | Leaf and MLS messages; core only | A new device gets a new key; it is not cloned |
| Device static Noise key (X25519) | Initiator of the channel with the realm; proof of possession is the handshake | Rotatable through a new credential signed by the root; separate from MLS signing, outer HPKE and root |
| Outer HPKE receiving key | Hide wrapping/headers per device | Rotate and keep the previous one only during a bounded queue window |
| Epoch and message secrets | MLS encryption/decryption | The library evolves and deletes them according to policy; do not export to recover history |
| Local data key | Encrypt DB, indexes and persisted secrets | Wrapped through the OS store; transactional migration on rotation |
| FileKey | One random key per file | Distributed inside MLS; do not reuse across files |
| Kit secret / archive secret | Open root recovery / history | High entropy, separate; losing them prevents opening each copy |
| Realm signing key (Ed25519) | Service identity; signs endpoint lists and the Noise key | Rotating it requires a new bootstrap of clients; does not change users' roots |
| Realm static Noise key (X25519) | Responder of the channel | Rotated by publishing a new list; the previous one is accepted during a bounded window |
| Endpoint TLS certificate | Traverse proxies and middleboxes | Optional; WebPKI or self-signed; does not take part in protocol security |
| Capabilities | Limited access to mailbox/blob | Revocable secret per scope; stored as a hash where verifiable |
A device credential binds all its public keys with their usages, the device ID and the identity. Do not reuse the bytes of an Ed25519 key as an X25519 encryption key for convenience. The profile of suites and encodings is fixed in PROTOCOL.
4. Logical server schema¶
Proposed tables: realm_memberships, invites, device_credentials, device_manifests, key_packages, mailboxes, capabilities, envelopes, blobs, push_subscriptions, endpoint_lists, schema_migrations. There is no sessions table: the session is the in-memory Noise channel, and authorization is resolved against device_credentials upon completing the handshake. No table contains conversations.
Essential constraints:
envelopesis unique per(mailbox_id, delivery_id), with a body hash to detect a conflicting retry. Never use a common message ID for all recipients.- ACK operates on concrete IDs and mailbox membership; it does not indiscriminately delete everything prior to a client cursor.
- An invitation token has its consumption and membership creation in one transaction.
- KeyPackages move
available → consumedatomically when claimed. A lost response may waste a package; it does not put it back into circulation. A malicious relay can still replay it and clients must detect that wherever they hold state. mailboxes.owner_device_idand quota are visible data: opacity of the ID does not mean anonymity of the owner.- Read, write and admin capabilities have independent scopes. Hashes of high-entropy random secrets, constant-time comparison, expiry and revocation.
- An envelope is confirmed to the sender only after durable commit. The body is not modified after acceptance.
- Complete blobs are immutable;
staging → committed → expired → deleted. GC ignores active uploads and coordinates with snapshots. - No message bodies, original file names, recovery keys, MLS secrets or local keys.
The server may keep minimal operational metadata, but it is never trusted for content authenticity. A compromised realm can forge consumption states, expiry and ACKs.
5. Local state and atomicity¶
The encrypted database contains contacts and their verified fingerprints, manifest maximums, MLS groups, policy, roster, events, per-recipient outbox, inbox/deduplication, routes and local archive. It must cover the MLS provider's storage, WAL, search indexes and temporary files.
Send unit: subsequent MLS state + local event + produced MLS bytes + persisted delivery attempts. If outer encryption happens afterwards, it starts from those same persisted bytes; the MLS send is never re-executed from a previous snapshot.
Receive unit: processed-delivery mark + MLS change + event/receipt + pending work. The transport ACK is emitted after the commit. Events from a future epoch can be persisted as pending before ACK, but not shown as authenticated until they can be validated.
Do not rely on two databases or two autonomous connections committing "almost at the same time". The feasibility proof must choose a provider with a shared transaction or design and review a durable journal with equivalent recovery.
Control commits have their own persistent state: prepared → selected → merged → welcome_released, with a single selection record per parent epoch in the coordinator profile. Do not confuse it with the application message outbox. See preparation and acceptance. The local database must meet the durability requirements.
6. States and transitions¶
Local delivery, per recipient¶
draft → queued_local → sealed_durable → relay_accepted → device_received
│ │ └→ read (optional)
├→ retry_wait └→ expired_or_unknown
└→ failed_action_required
relay_accepted comes from the server and may be false if it is malicious. device_received requires an E2EE receipt from that device. read only occurs if receipts are enabled and never proves human attention. A group's aggregate shows pending/partial; it does not turn one device's ACK into receipt by all. A removed device stops being counted for future messages, without rewriting the evidence of earlier ones.
Drafts can be edited freely; an already sealed event is immutable. Editing a sent message, if added after V1, will be another E2EE event with explicit authorization and reference.
Device¶
created_local → root_authorized → realm_registered → group_joined
└→ revoked └→ removed_from_group
These are related axes, not a single global state: a device can be registered and belong to no group, or be removed from the realm but keep old messages. revoked is terminal for that credential; recovery generates a new ID and keys.
Group¶
creating → active → awaiting_commit → active
├→ needs_resync → fresh_rejoin
└→ fork_suspected → paused → verified_new_group
MLS secret forks are not merged with "last write wins" rules. Old history can remain readable while sending to the affected group is suspended.
7. Domain invariants¶
- Identity does not depend on the name, hostname or membership on the server.
- A valid signature attests who authorized bytes; it does not imply freshness or authorization of any action.
- Each leaf is bound to an authorized device; one person can occupy several leaves.
- The server does not add devices to a group by modifying its directory.
- Clients do not roll back known versions or resurrect MLS states from backups.
- Recovering identity, recovering history and rejoining groups are separate operations.
- A local deletion or a TTL does not promise remote deletion of copies.
- The UI derives its states from local facts, relay acceptance and authenticated receipts separately.
References and review scope: README.
Personal invitation persistence (implemented)¶
Relay schema 5 adds issued_invitations, invitation_audit and
key_package_claim_receipts; role owner now authorizes invitation operations
and is assigned explicitly on the host. Profile schema 8 adds
invitation_operations and invitation_hellos. Pending link/token/contact
secrets live in that encrypted profile, not in Flutter preferences. MLS,
conversation, outbox and invitation→group mapping share one transaction.
See lifecycle, exports and retention.