Architecture

Three components and one spec. Each piece does one job, can be swapped out, and doesn’t know more than it has to about the others.

The components

  • trueseal-noise — Noise Protocol channels (XX + NK). Knows nothing about sync.
  • trueseal-sync — the sync engine. Identity, pairing, group manifest, addressed encryption, outbox.
  • trueseal-relay — a zero-knowledge blob router. Holds ciphertext for offline recipients and delivers it when they come back. Self-hostable.
  • trueseal-protocol — the wire spec everyone speaks. Just docs, no library.

Who owns what

ConcernOwner
Noise Protocol handshakestrueseal-noise
Encrypted transport channelstrueseal-noise
Device identity (keypair generation, persistence)trueseal-sync
Pairing ceremonytrueseal-sync
Group manifest (membership, versioning)trueseal-sync
Envelope construction and addressed encryptiontrueseal-sync
Operation log and outbox replaytrueseal-sync
Blob routing and deferred deliverytrueseal-relay
Blob retention and TTL reapingtrueseal-relay
Wire protocol specificationtrueseal-protocol

What the relay actually sees

Two things, and that’s it:

  • Which noise keys have an active Receive Session. Unavoidable. If you want online devices to get their messages immediately, the relay has to know who’s online.
  • The recipient public key on each envelope. It needs that to route the blob to the right inbox.

It doesn’t see who sent a blob (push sessions use a throwaway keypair, so the sender is unlinkable), it has no concept of groups, and it can’t read payloads because everything is encrypted to the recipient before it ever hits the wire.

Compromise the relay completely — grab the binary, dump the database, sniff every packet — and you walk away with a list of public keys that received blobs. Nothing else.

Sending a blob

SENDER DEVICE
groups Read Manifest
lock Encrypt per recipient
draw Sign envelope
arrow_forward
send RELAY PUSH SESSION (NOISE NK)
sender: anonymousfan-out: N envelopes
arrow_forward
RECIPIENT DEVICE
inbox Inbox if offline
bolt Deliver if online

When a device calls send():

  1. Read the Group Manifest. trueseal-sync pulls the current list of members and their noise + signing public keys.

  2. Build one Envelope per recipient. For each member, trueseal-sync does an ephemeral X25519 key agreement against the recipient’s static public key, then encrypts the payload with ChaCha20-Poly1305. The sender’s author_pub (Ed25519 signing key) gets prepended inside the plaintext, so it’s invisible to the relay. Then the envelope is signed over sequence || parent_hashes || recipient_pub || ciphertext with the sender’s Ed25519 key.

    The result: a blob the relay can route (it sees recipient_pub) but can’t read or tamper with undetected.

  3. Open an anonymous Push Session. A fresh ephemeral X25519 keypair is generated just for this push, and Noise NK is used to connect to the relay. NK authenticates the relay (the device verifies the relay’s static public key) but transmits no stable client identity. From the relay’s point of view it’s an unlinkable peer, with no way to connect this push to any device or Receive Session.

  4. Send all N envelopes over the same Push Session, then close it.

  5. Relay stores or forwards. Each envelope goes into the recipient’s Inbox. If that recipient has an active Receive Session, it’s handed over immediately. Otherwise it sits there until they reconnect.

  6. Outbox tracks delivery. Every envelope is marked undelivered in the local Operation Log until the relay confirms receipt. If the sender drops offline before that confirmation, the envelope rides along on the next reconnect.

Receiving a blob

RELAY INBOX
all_inbox Held blobs
outbox Stream new blobs
arrow_forward
swap_horiz RECEIVE SESSION NOISE XX (MUTUAL AUTH)
session: long-liveddelete: on DeliverAck
arrow_forward
RECEIVER DEVICE
lock_open Decrypt + verify
fact_check Check manifest
check_circle DeliverAck

When a device connects to the relay:

  1. Open a Receive Session. A Noise XX handshake using the device’s stable noise keypair. Both sides authenticate each other. The relay marks the device online and starts forwarding blobs as they come in.

  2. Drain the inbox. Any blobs being held for the device get delivered over the session. They’re not deleted yet — deletion waits for the ack.

  3. Send DeliverAck. Once the client has durably written each blob, it sends a DeliverAck with the blob’s ID and the relay deletes it. If the session drops before the ack lands, the blob survives and gets re-delivered next time. Clients have to be ready for duplicates.

  4. Decrypt and verify. For each envelope, trueseal-sync decrypts with the device’s private key, peels author_pub off the first 32 bytes of plaintext, verifies the signature against it, and finally checks author_pub against the current Group Manifest. Anything from a device not in the manifest gets dropped on the floor.

  5. Fire the callback. The plaintext lands in the caller’s on_message handler. The caller doesn’t touch keys, sessions, or signatures at any point.

Pairing

HOST INITIATOR
qr_code_2 Generate Token
share Share out-of-band
task_alt Accept request
arrow_forward
hub RELAY OPAQUE BLOB ROUTING
visibility: recipient_pub onlyknows: nothing about pairing
arrow_forward
JOINER DEVICE
qr_code_scanner Read Token
send Send Pair message

Before two devices can sync anything, they need each other’s public keys. That’s pairing:

  1. The initiating device generates a Pairing Token — its noise public key and its Ed25519 signing public key, encoded opaquely. It gets passed out-of-band (QR code, AirDrop, link, whatever fits).

  2. The joining device reads the token and sends a Pair message: an envelope addressed to the initiator, encrypted with the initiator’s public key, containing the joiner’s own keys.

  3. The initiator gets the Pair message, fires on_member_request, and the caller calls accept_member() to admit the new device.

  4. A fresh Group Manifest listing both members is signed by the accepting device and pushed to everyone in the group.

The relay’s role here is just step 2: it sees one blob addressed to the initiator and routes it. It has no idea that blob happens to be a pairing request.