buenomini

trueseal

End-to-end encrypted sync for Swift, Kotlin and TypeScript apps, with a relay you run yourself.

E2EE sync SDK and relaytrueseal.devdocsgithub
status
Heading to the 0.6.0 developer preview
core
Rust, with Swift and Kotlin bindings via UniFFI
typescript
Node SDK via napi-rs (no browser yet)
relay
Go, one container, self-hosted
transport
Noise XX and NK over TCP
license
Apache-2.0

trueseal is the private sync layer an app builds on: pairing, keys, the offline outbox and delivery are already in there, so you get end-to-end encrypted sync across devices in about ten lines. There are no accounts. Your devices talk through a relay you run yourself, and the relay stores and forwards blobs it has no keys to read.

It started because I wanted a clipboard with history (that's hush-clip now), and it grew out of the sync layer inside hush. This is the project I spend most of my time on.

The Rust core is at v0.5.0 and the next release is the 0.6.0 developer preview. Nothing is on npm or Maven yet, and 0.x can still break things between releases. The threat model, the limitations and everything else live in the docs, so this page stays short.

NK pushXX deliverackYour appdevice Atrueseal-syncRust coreRelayGo, self-hostedstores until acktrueseal-syncRust coreYour appdevice BQR codein person NK pushXX deliverackYour appdevice Atrueseal-syncRust coreRelayGo, self-hostedstores until acktrueseal-syncRust coreYour appdevice BQRin person
Figure 1. How a message travels. The relay only ever holds ciphertext, and devices pair in person with a QR code.

# Decisions

Each of these has an ADR in the repos.

A delivery primitive with no conflict resolution

sync ADR-0003
chose
trueseal delivers messages. The app owns its data model, its schema and what happens when two devices disagree, which is the same contract TCP has with its callers.
rejected
Resolving conflicts inside trueseal.
cost
Every app writes its own conflict policy. hush-clip does last-writer-wins.

Delete only after the device says it has it

relay ADR-0009
chose
Blobs stay on the relay until the recipient sends DeliverAck.
rejected
Treating a successful TCP write as delivered. That only means the bytes left the relay's buffer.
cost
A breaking wire change, and clients have to deduplicate re-deliveries.

Rust for the client, Go for the relay

sync ADR-0008
chose
A Rust core with UniFFI bindings for Swift and Kotlin. The relay stays in Go.
rejected
Go everywhere. Except it can't ship as a library inside an iOS app.
cost
Noise now exists twice, in Go and Rust, checked against the same official test vectors. Also the borrow checker.

Written along the way