flowchart TB
accTitle: Plaintext exists only on the desktop and inside an approved request
accDescr: One account key. The desktop encrypts messages before upload, and each reader or writer opens its own copy. The desktop app reads the sources on the desktop (mail, chats, calendars), makes and keeps the key, encrypts each message and computes search tags. It sends ciphertext and tags, and attachments as they are, to the server. Readers and writers (AI assistants, the web app, second devices and scripts), each with its own token, send a token and request to the server. The server finds the token's copy, opens the key per request, searches and decrypts hits, returns the results and drops the key afterwards. It stores and looks up ciphertext, search tags, token hashes and locked copies of the key in the database, and stores and reads attachments in the blob store, encrypted under the storage key, which is sent with each request and never kept. The server never stores the account key, a token or plaintext. The blob store sees each attachment in the clear while it handles a request, and keeps only ciphertext.
S["Sources on the desktop<br/>mail, chats, calendars"] -->|reads| D["Desktop app<br/>makes and keeps the key<br/>encrypts each message<br/>computes search tags"]
R["Readers and writers<br/>AI assistants, web app<br/>second devices, scripts<br/>each with its own token"]
D -->|"ciphertext + tags,<br/>attachments as they are"| V["Server<br/>finds the token's copy<br/>opens the key per request<br/>searches, decrypts hits<br/>drops the key afterwards"]
R -->|token and request| V
V -.->|results| R
V <-->|store, look up| B[("Database<br/>ciphertext, search tags<br/>token hashes<br/>locked copies of the key")]
V <-->|store, read| O[("Blob store<br/>attachments, encrypted<br/>under the storage key,<br/>sent with each request<br/>and never kept")]
B ~~~ N["The server never stores the account<br/>key, a token or plaintext. The blob<br/>store sees each attachment in the<br/>clear while it handles a request,<br/>and keeps only ciphertext."]:::note
classDef note fill:none,stroke:none,color:#5f7183
Appendix B — The diagrams as text
Every diagram in this document, written out: a description in words, for screen readers and search, and its source in Mermaid, which GitHub draws and anyone can edit in a pull request. The pictures in the chapters are exported from the editor’s document; these text versions are kept in step with them, and the sync tool reports any picture that has changed since its text version was last checked.
Figure 4.1. The whole design: one account key, one kind of token, a database and a blob store
Plaintext exists only on the desktop and inside an approved request
In 4. The design on one page. One account key. The desktop encrypts messages before upload, and each reader or writer opens its own copy. The desktop app reads the sources on the desktop (mail, chats, calendars), makes and keeps the key, encrypts each message and computes search tags. It sends ciphertext and tags, and attachments as they are, to the server. Readers and writers (AI assistants, the web app, second devices and scripts), each with its own token, send a token and request to the server. The server finds the token’s copy, opens the key per request, searches and decrypts hits, returns the results and drops the key afterwards. It stores and looks up ciphertext, search tags, token hashes and locked copies of the key in the database, and stores and reads attachments in the blob store, encrypted under the storage key, which is sent with each request and never kept. The server never stores the account key, a token or plaintext. The blob store sees each attachment in the clear while it handles a request, and keeps only ciphertext.
Figure 5.1. Where the account key lives in the clear, and where only locked copies exist
The account key exists in the clear in only two places
In 5. How it works. Where the account key lives, where only a locked copy lives, and where it never goes. In the clear, the key exists only in the desktop OS keystore, at rest on each approved desktop and made on the first one, and in server memory, with its two derived keys, for one approved request only. Only a locked copy is in the database, one copy per approved token wrapped under a key from it, and in backups, where deleted copies stay until the backup expires, up to 30 days. The AIs, board and scripts hold a token but never the key, and the server unlocks it for them. AI providers see only the results they asked for. Logs and operator staff hold no key, no token and no plaintext. A token opens only its own copy, so deleting that copy cuts its holder off at once.
flowchart TB
accTitle: The account key exists in the clear in only two places
accDescr: Where the account key lives, where only a locked copy lives, and where it never goes. In the clear, the key exists only in the desktop OS keystore, at rest on each approved desktop and made on the first one, and in server memory, with its two derived keys, for one approved request only. Only a locked copy is in the database, one copy per approved token wrapped under a key from it, and in backups, where deleted copies stay until the backup expires, up to 30 days. The AIs, board and scripts hold a token but never the key, and the server unlocks it for them. AI providers see only the results they asked for. Logs and operator staff hold no key, no token and no plaintext. A token opens only its own copy, so deleting that copy cuts its holder off at once.
subgraph never["Never holds the key"]
direction TB
N1["AIs, board and scripts<br/>hold a token, never the<br/>key, the server unlocks<br/>it for them"]
N2["AI providers<br/>see only the results<br/>they asked for"]
N3["Logs and operator staff<br/>no key, no token,<br/>no plaintext"]
N1 ~~~ N2 ~~~ N3
end
subgraph locked["Only a locked copy"]
direction TB
L1["Database<br/>one copy per approved<br/>token, wrapped under<br/>a key from it"]
L2["Backups<br/>deleted copies stay<br/>until the backup expires,<br/>up to 30 days"]
L1 ~~~ L2
end
subgraph clear["In the clear"]
direction TB
K1["Desktop OS keystore<br/>the key at rest, on each<br/>approved desktop, made<br/>on the first one"]
K2["Server memory<br/>the key and its two<br/>derived keys, for one<br/>approved request only"]
K1 ~~~ K2
end
classDef hot stroke:#c8932e,fill:#fbf3e3,color:#1f2933
class K1,K2 hot
Figure 5.2. Create: the desktop makes the account key and uploads only a locked copy
The account key is born on the desktop, and only a locked copy reaches the server
In 5. How it works. Create. The desktop app generates the account key from 256 random bits and stores it in the OS keystore. It signs in and asks the server for a desktop token, and the server replies with one. The desktop app wraps the account key under a key derived from the token and sends the locked copy to the server, which stores the token’s hash and the copy in the database. The account key never leaves the desktop in the clear.
sequenceDiagram accTitle: The account key is born on the desktop, and only a locked copy reaches the server accDescr: Create. The desktop app generates the account key from 256 random bits and stores it in the OS keystore. It signs in and asks the server for a desktop token, and the server replies with one. The desktop app wraps the account key under a key derived from the token and sends the locked copy to the server, which stores the token's hash and the copy in the database. The account key never leaves the desktop in the clear. participant D as Desktop app participant S as Server participant DB as Database Note over D: generate the account key from 256<br/>random bits Note over D: store the account key in the OS<br/>keystore D->>S: sign in, ask for a desktop token S-->>D: a desktop token Note over D: wrap the account key under a key<br/>derived from the token D->>S: the locked copy S->>DB: store the token's hash and the copy Note over D: The account key never leaves the<br/>desktop in the clear.
Figure 5.3. Read: a token opens its copy of the account key for one request
The account key is unwrapped for one request and never written down
In 5. How it works. Read. A reader or writer sends a request carrying its token. The server looks up the token’s hash in the database and gets back its locked copy. A key derived from the token unwraps the account key, in memory only. The server looks up the query’s tags and gets back ciphertext rows, decrypts the candidates and builds the answer, and returns the results. The request ends and the account key is dropped.
sequenceDiagram accTitle: The account key is unwrapped for one request and never written down accDescr: Read. A reader or writer sends a request carrying its token. The server looks up the token's hash in the database and gets back its locked copy. A key derived from the token unwraps the account key, in memory only. The server looks up the query's tags and gets back ciphertext rows, decrypts the candidates and builds the answer, and returns the results. The request ends and the account key is dropped. participant R as Reader or writer participant S as Server participant DB as Database R->>S: a request carrying its token S->>DB: look up the token's hash DB-->>S: its locked copy Note over S: a key derived from the token<br/>unwraps the account key, in memory<br/>only S->>DB: look up the query's tags DB-->>S: ciphertext rows Note over S: decrypt the candidates, build the<br/>answer S-->>R: the results Note over S,DB: The request ends and the account<br/>key is dropped.
Figure 5.4. Update: a new reader or writer gets its own token and its own locked copy
A new holder gets its own copy, made inside the user’s session
In 5. How it works. Update. The user approves a new reader or writer, inside a session that holds the account key. The server makes a new token and wraps the account key under a key derived from it, stores the new token’s hash and the copy in the database, and hands the new token to the reader or writer once. Renewal runs the same steps, then deletes the old token’s hash and copy.
sequenceDiagram accTitle: A new holder gets its own copy, made inside the user's session accDescr: Update. The user approves a new reader or writer, inside a session that holds the account key. The server makes a new token and wraps the account key under a key derived from it, stores the new token's hash and the copy in the database, and hands the new token to the reader or writer once. Renewal runs the same steps, then deletes the old token's hash and copy. participant U as User participant R as Reader or writer participant S as Server participant DB as Database U->>S: approve, inside a session that holds the account key Note over S: make a new token, wrap the account<br/>key under a key derived from it S->>DB: store the new token's hash and the copy S-->>R: the new token, handed over once Note over R,DB: Renewal runs the same steps, then<br/>deletes the old token's hash and<br/>copy.
Figure 5.5. Expire: an expired token is refused and its copy is deleted
An expired token finds no copy and is refused
In 5. How it works. Expire. The server finds copies past their expiry in the database and deletes each one’s hash and copy. When a reader or writer then sends a request with the expired token, the server refuses it, and the holder must renew or be approved again. The account key itself is rotated before its nonce budget runs out, by re-encrypting or by re-wrapping data keys (questions 2 to 4).
sequenceDiagram accTitle: An expired token finds no copy and is refused accDescr: Expire. The server finds copies past their expiry in the database and deletes each one's hash and copy. When a reader or writer then sends a request with the expired token, the server refuses it, and the holder must renew or be approved again. The account key itself is rotated before its nonce budget runs out, by re-encrypting or by re-wrapping data keys (questions 2 to 4). participant R as Reader or writer participant S as Server participant DB as Database S->>DB: find copies past their expiry S->>DB: delete each one's hash and copy R->>S: a request with the expired token S-->>R: refused, renew or re-approve Note over R,DB: The account key is rotated before<br/>its nonce budget runs out, by<br/>re-encrypting or re-wrapping data<br/>keys (questions 2 to 4).
Figure 5.6. Delete: access ends when a token’s hash and its copy are deleted
Deleting copies is how access ends
In 5. How it works. Delete. The user revokes a token, or deletes the account. The server deletes that token’s hash and copy, or every copy and row. Backups keep old copies until they expire, up to 30 days. If every holder is lost, the next desktop makes a new account key and collects again.
sequenceDiagram accTitle: Deleting copies is how access ends accDescr: Delete. The user revokes a token, or deletes the account. The server deletes that token's hash and copy, or every copy and row. Backups keep old copies until they expire, up to 30 days. If every holder is lost, the next desktop makes a new account key and collects again. participant U as User participant S as Server participant DB as Database U->>S: revoke a token, or delete the account S->>DB: delete the hash and copy, or every copy and row Note over S,DB: Backups keep old copies until they<br/>expire, up to 30 days. Note over U: If every holder is lost, the next<br/>desktop makes a new account key<br/>and collects again.
Figure 5.7. Collect and push: messages are encrypted on the desktop before upload
Messages are encrypted on the desktop before they leave it
In 5. How it works. Collect and push. The desktop app reads a new message from a local source, such as mail, chat or calendar. It encrypts the message under the account key and computes its search tags. It sends the ciphertext and tags to the server over TLS, with its token. The server stores them and cannot read them. The message is searchable as soon as it is stored.
sequenceDiagram accTitle: Messages are encrypted on the desktop before they leave it accDescr: Collect and push. The desktop app reads a new message from a local source, such as mail, chat or calendar. It encrypts the message under the account key and computes its search tags. It sends the ciphertext and tags to the server over TLS, with its token. The server stores them and cannot read them. The message is searchable as soon as it is stored. participant D as Desktop app participant S as Server participant DB as Database Note over D: read a new message from a local<br/>source, mail, chat or calendar Note over D: encrypt it under the account key,<br/>compute its search tags D->>S: ciphertext and tags over TLS, with its token S->>DB: store them, the server cannot read them Note over S,DB: The message is searchable as soon<br/>as it is stored.
Figure 5.8. Approve a reader or writer: one approval, one token, one locked copy
Approving a reader or writer creates one token and one copy of the account key
In 5. How it works. Approve a reader or writer. The user approves it by OAuth consent, by the web app opened from the desktop, or with a matching code. Inside the user’s session, the server makes a token and a copy of the account key locked under it, stores the token’s hash and the copy in the database, and hands the token to the reader or writer once.
sequenceDiagram accTitle: Approving a reader or writer creates one token and one copy of the account key accDescr: Approve a reader or writer. The user approves it by OAuth consent, by the web app opened from the desktop, or with a matching code. Inside the user's session, the server makes a token and a copy of the account key locked under it, stores the token's hash and the copy in the database, and hands the token to the reader or writer once. participant U as User participant R as Reader or writer participant S as Server participant DB as Database U->>S: approve by OAuth consent, the web app opened from the desktop, or a matching code Note over S: inside the user's session, make a<br/>token and a copy of the account<br/>key locked under it S->>DB: store the token's hash and the copy S-->>R: the token, handed over once
Figure 5.9. Search and retrieve: the account key is opened for one request only
The account key is unlocked for one request and dropped when it ends
In 5. How it works. Search and retrieve. A reader or writer sends a query with its token. The server finds the token’s copy by its hash, unlocks the account key for this request and turns the query into tags. It finds the candidates in the index and gets back candidate rows, still ciphertext. It decrypts only the candidates, checks the real match, and returns the results. The request ends and the account key is dropped.
sequenceDiagram accTitle: The account key is unlocked for one request and dropped when it ends accDescr: Search and retrieve. A reader or writer sends a query with its token. The server finds the token's copy by its hash, unlocks the account key for this request and turns the query into tags. It finds the candidates in the index and gets back candidate rows, still ciphertext. It decrypts only the candidates, checks the real match, and returns the results. The request ends and the account key is dropped. participant R as Reader or writer participant S as Server participant DB as Database R->>S: a query, with its token S->>DB: find the token's copy by its hash Note over S: unlock the account key for this<br/>request, turn the query into tags S->>DB: find the candidates in the index DB-->>S: candidate rows, still ciphertext Note over S: decrypt only the candidates, check<br/>the real match S-->>R: the results Note over S,DB: The request ends and the account<br/>key is dropped.
Figure 5.10. Update: a writer’s change is encrypted on the server inside its request
A writer’s change is encrypted on the server and seen on the next read
In 5. How it works. Update. A reader or writer sends a change with its token, such as an AI tool call or a web-app edit. The server unlocks the account key, encrypts the change, recomputes its tags and stores it. On any reader’s next request, the server returns the updated message.
sequenceDiagram accTitle: A writer's change is encrypted on the server and seen on the next read accDescr: Update. A reader or writer sends a change with its token, such as an AI tool call or a web-app edit. The server unlocks the account key, encrypts the change, recomputes its tags and stores it. On any reader's next request, the server returns the updated message. participant R as Reader or writer participant S as Server participant DB as Database R->>S: a change with its token, an AI tool call or a web-app edit Note over S: unlock the account key, encrypt<br/>the change, recompute its tags S->>DB: store it R->>S: any reader's next request S-->>R: the updated message
Figure 5.11. Revoke and recover: one deletion cuts a holder off, and recovery needs another approved device
Revoking deletes a copy, and recovery needs another approved device
In 5. How it works. Revoke and recover. The user revokes a token, or a lost desktop, and the server deletes its hash and its copy in the database. A new desktop asks the server to join, the user approves the new desktop, and the server gives the new desktop its own token, which opens its own copy of the account key. If every holder is lost, the mirror cannot be read, and a new desktop makes a new account key and collects again from the sources.
sequenceDiagram accTitle: Revoking deletes a copy, and recovery needs another approved device accDescr: Revoke and recover. The user revokes a token, or a lost desktop, and the server deletes its hash and its copy in the database. A new desktop asks the server to join, the user approves the new desktop, and the server gives the new desktop its own token, which opens its own copy of the account key. If every holder is lost, the mirror cannot be read, and a new desktop makes a new account key and collects again from the sources. participant U as User participant D as Desktop app participant S as Server participant DB as Database U->>S: revoke a token, or a lost desktop S->>DB: delete its hash and its copy D->>S: a new desktop asks to join U->>S: approve the new desktop S-->>D: its own token, which opens its own copy of the account key Note over D,DB: If every holder is lost, the<br/>mirror cannot be read. A new<br/>desktop makes a new account key<br/>and collects again from the<br/>sources.
Figure 6.1. Latency budget: targets at the 95th percentile, not measurements
Encryption adds well under a second, and the answer lands inside the 10-second budget
In 6. Latency budget. Question to first response is 7.4 seconds at the 95th percentile, of which the mirror is 0.405 seconds. These are targets, not measurements. The AI provider reads the question and calls the mirror in an assumed 3 seconds. The mirror then takes 0.405 seconds of its 1-second budget: 0.2 seconds for the request to the server and back over the network, 0.005 seconds on the server to find the token and unlock the key, 0.1 seconds in the database to look up the query’s tags, and 0.1 seconds on the server to decrypt and check 200 candidates. The AI provider then writes the first words from the results in an assumed 4 seconds, so the first response arrives at 7.4 seconds, inside the 10-second budget.
flowchart TB
accTitle: Encryption adds well under a second, and the answer lands inside the 10-second budget
accDescr: Question to first response is 7.4 seconds at the 95th percentile, of which the mirror is 0.405 seconds. These are targets, not measurements. The AI provider reads the question and calls the mirror in an assumed 3 seconds. The mirror then takes 0.405 seconds of its 1-second budget: 0.2 seconds for the request to the server and back over the network, 0.005 seconds on the server to find the token and unlock the key, 0.1 seconds in the database to look up the query's tags, and 0.1 seconds on the server to decrypt and check 200 candidates. The AI provider then writes the first words from the results in an assumed 4 seconds, so the first response arrives at 7.4 seconds, inside the 10-second budget.
Q(["The user asks a question · 0 s"]) --> A["AI provider reads the question<br/>and calls the mirror<br/>3 s, assumed"]
A --> M
subgraph M["The mirror · 0.405 s of its 1 s budget"]
direction TB
M1["Request to the server and back<br/>network · 0.2 s"] --> M2["Find the token and unlock the key<br/>server · 0.005 s"] --> M3["Look up the query's tags<br/>database · 0.1 s"] --> M4["Decrypt and check 200 candidates<br/>server · 0.1 s"]
end
M --> W["AI provider writes the first<br/>words from the results<br/>4 s, assumed"]
W --> F(["First response at 7.4 s,<br/>inside the 10 s budget"])
F ~~~ N["Targets at the 95th percentile,<br/>not measurements."]:::note
classDef mirror stroke:#2f5f86,fill:#e8eff6,color:#1f2933
class M1,M2,M3,M4 mirror
classDef note fill:none,stroke:none,color:#5f7183