Mailbox records and how they relate
Glitr does not rent you a database. It stores structured documents in a git repository and queries them like a small app schema. This page is the record model: what kinds of files exist, which ones a contact can fetch, and what a ciphertext field actually holds.
It is research and development — subject to change. You should not edit these files by hand.
Three layers, so the password is not confused with message encryption:
- The store — a schema stamp, documents vs collections, optional at-rest sealing.
- The messaging types — profile, contacts, outbound, inbox, sent, receipts, signaling, protocol sessions.
- The wire inside
ciphertext— cascade (current) or an older hybrid envelope.
The user-facing mailbox story is Git as your mailbox. What a host can infer is encryption visibility.
The store
On first connect, Glitr writes a schema stamp into the repository (schema.json and schemaVersion.json). Those stamps stay readable. They name the app (messaging) and a version number. If two clients disagree on shape, a migration can rewrite files; as a user you mostly notice “please update the app.”
| Kind | On disk | Messaging examples |
|---|---|---|
| Document | One singleton file | profile-public.json, profile-secrets.json |
| Collection | A folder of rows, one file per id | outbound/{id}.json, contacts/{id}.json |
| Sealed type | Same path, but the file is a password envelope | Contacts, inbox, sent, secrets, protocol sessions |
Sealing (@encrypted in the schema) is not end-to-end messaging. On flush, Glitr derives a key from your connect password (Argon2id) and wraps the JSON in AES-GCM. GraphQL inside the running app still sees plaintext. A clone of your repo without that password sees only the envelope.
Types without sealing are ordinary JSON. Contacts are meant to fetch those.
What is visible vs sealed
| Visible on the git remote | Sealed with your connect password |
|---|---|
profile-public.json | profile-secrets.json |
outbound/ | contacts/ |
readreceipts/ | inbox/ |
signaling/ | sent/ |
protocol-sessions/ |
Peers poll your public profile, outbound, read receipts, and signaling. They do not open your sealed folders.
Messaging types (v3)
The app schema is messaging, version 3. Fields below are in plain language — not a GraphQL listing.
Profile ids are how two repositories address each other (senderId, recipientId, peerProfileId). Contact ids are local only: “this person in my address book.”
ProfilePublic — one file, peer-visible
Your public identity. Holds an RSA public key and, in v3, protocol bundles: the Signal, PQXDH, and ML-KEM public material a contact needs to encrypt the first message to you.
ProfileSecrets — one file, sealed
Your display name, handle, RSA private key, and matching protocol secrets. Never something a peer poll reads.
Contact — collection, sealed
Someone you added: their profile id, the git URL you fetch, a display name, optional git credentials, last poll time, and connection status.
OutboundMessage — collection, peer-visible
Mail on your repo. Addressed to a recipientId. The payload that matters is ciphertext (cascade or hybrid). A body field may exist as a local convenience; peers decrypt the ciphertext, not that column. ackReadReceiptIds notes receipts you have already taken into account.
ReadReceipt — collection, peer-visible
Lives on the reader’s repository. Points at an outbound messageId and who marked it read. Live receipts can skip git and travel on the data channel instead (live links).
InboxMessage — collection, sealed
Your local copy of a decrypted inbound line: plaintext body, who sent it, which outbound id it came from, which contact it is filed under.
SentMessage — collection, sealed
Your local archive of what you sent: plaintext, contact id, outbound id, timestamps, and when it was marked read.
SignalingSession — collection, peer-visible
One row per contact while you are connected. Holds cascaded WebRTC handshake material (offer or answer). Logout deletes the row.
ProtocolSession — collection, sealed
One row per contact. Stores ratchet state (Signal and PQXDH) so later messages do not redo the handshake. Git and live share this session so the ratchet does not fork.
Git cannot consume one-time prekeys off a peer repository the way a live prekey server would. The signed prekey is the last-resort path. Handshake wording: Signal and post-quantum.
Lifecycle of a git message
Send writes Outbound (readable ciphertext) and Sent (sealed plaintext). Their poll copies addressed outbound into Inbox. Marking read writes ReadReceipt on their repo. Your later poll can remove the outbound file. That is how ticks move from sent to delivered to read on the git path.
Layout in a repository
Each collection row is {folder}/{id}.json. Schema stamps stay plaintext. Sealed folders still have those paths; the file contents are envelopes.
Schema versions
- v1 — profiles, contacts, outbound, receipts, inbox, sent.
- v2 — adds signaling so git can broker a live link.
- v3 — adds public protocol bundles, private protocol secrets, and per-contact ProtocolSession so the cascade can ratchet.
Old outbound rows that still look like { wrappedKey, nonce, ciphertext } still decrypt (hybrid fallback). New sends use the cascade envelope.
What sits in ciphertext
Outbound, signaling, and live chat bodies share the same recipient encryption. Detection is by shape, not by folder name.
Cascade (current) — a JSON object with v: 2 and scheme: "cascade". It names the layers (AES, RSA hybrid, Signal, ML-KEM, PQXDH), holds the outermost ciphertext, and on a first message may include Signal and PQXDH handshake blobs. Later messages ratchet using the stored ProtocolSession and omit those handshake fields.
Hybrid (v1 fallback) — { wrappedKey, nonce, ciphertext }. RSA-OAEP-4096 wraps a one-shot AES-256-GCM key. Used when a peer has no protocol bundles yet, and still accepted on ingest so old rows open.
Password envelopes on sealed files are a different JSON (kdf, aead, salt, nonce, ciphertext). They never travel as the recipient payload.
How to read this without mixing layers
- A git host sees files and traffic. Sealed folders are envelopes. Outbound and signaling are ciphertext meant for a contact, not for the host. See what a host can see.
- Your password opens your mailbox documents. It does not encrypt a message to someone.
- The cascade is how a payload is addressed to a person. Architecture of that stack: Architecture and Signal and post-quantum.