Skip to main content

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.

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

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

Figure A.1. One shared database compared with one database per user

Keys isolate stored data either way, and separate instances contain a breach

In Appendix A. Single or multi-tenant. Each user’s key stays on their desktop in both designs, and colours mark whose key locks which rows. With one shared database, users A, B and C, holding keys A, B and C, all use one shared server, which opens key A, B or C inside each user’s request, and one shared database with row-level security, where user A’s rows are locked under key A, user B’s under key B and user C’s under key C. A server breach sees every active user’s requests. There is one system to run, and metadata is visible to the operator. With one instance per user, user A uses server A, which holds key A only, and database A, which holds user A’s rows under key A, and the same for users B and C. A server breach sees one user’s requests. There is one system per user to run and pay for.

flowchart TB
  accTitle: Keys isolate stored data either way, and separate instances contain a breach
  accDescr: Each user's key stays on their desktop in both designs, and colours mark whose key locks which rows. With one shared database, users A, B and C, holding keys A, B and C, all use one shared server, which opens key A, B or C inside each user's request, and one shared database with row-level security, where user A's rows are locked under key A, user B's under key B and user C's under key C. A server breach sees every active user's requests. There is one system to run, and metadata is visible to the operator. With one instance per user, user A uses server A, which holds key A only, and database A, which holds user A's rows under key A, and the same for users B and C. A server breach sees one user's requests. There is one system per user to run and pay for.
  subgraph shared["One shared database"]
    direction TB
    SA["User A<br/>holds key A"]:::a
    SB["User B<br/>holds key B"]:::b
    SC["User C<br/>holds key C"]:::c
    SA & SB & SC --> SS["One shared server<br/>opens key A, B or C<br/>inside each user's request"]
    SS --> SD[("One shared database,<br/>with row-level security<br/>A's rows under key A<br/>B's rows under key B<br/>C's rows under key C")]
  end
  subgraph dedicated["One instance per user"]
    direction TB
    UA["User A<br/>holds key A"]:::a --> VA["Server A<br/>key A only"] --> BA[("Database A<br/>A's rows<br/>under key A")]:::a
    UB["User B<br/>holds key B"]:::b --> VB["Server B<br/>key B only"] --> BB[("Database B<br/>B's rows<br/>under key B")]:::b
    UC["User C<br/>holds key C"]:::c --> VC["Server C<br/>key C only"] --> BC[("Database C<br/>C's rows<br/>under key C")]:::c
  end
  shared ~~~ dedicated
  classDef a stroke:#2f6fd0,fill:#e3edfb,color:#1f2933
  classDef b stroke:#e0663a,fill:#fbe9e2,color:#1f2933
  classDef c stroke:#2a9d6f,fill:#e0f3ea,color:#1f2933