efema 0.3.0

The efema client: sync sealed changes between devices through a relay that cannot read them
Documentation

Your devices leave their changes for each other in one place: efema keeps them in order, hands each device what it missed, and cannot read any of it.

Why

A local-first app keeps your data on your device and works without a network. Then you install it on a second device, and the two need somewhere to leave their changes for each other. The usual answers are a sync service that reads everything it carries, or a database server that has to understand your data to store it.

efema is the third answer: a relay that knows nothing about your data, and a client library that seals every change on the device before it leaves. The relay keeps sealed entries in one order for everyone and hands each device what came after its cursor. It never merges, never decides who is right, and cannot read what it stores.

A day in the life

A relay, and a folder synced between a laptop and a desktop by mini-vault, the example app. Both edit plan.md before hearing from each other, and both versions survive:

$ mini-vault --relay http://127.0.0.1:2451 --folder ./laptop --state ./laptop-state sync --once
applied 0 change(s), sent 3
$ mini-vault --relay http://127.0.0.1:2451 --folder ./desktop --state ./desktop-state sync --once
applied 3 change(s), sent 0
$ mini-vault --relay http://127.0.0.1:2451 --folder ./desktop --state ./desktop-state sync --once
applied 0 change(s), sent 1
$ mini-vault --relay http://127.0.0.1:2451 --folder ./laptop --state ./laptop-state sync --once
applied 0 change(s), sent 2
conflict: both versions kept, the other one as notes/plan (conflict 249b3cca).md
$ mini-vault --relay http://127.0.0.1:2451 --folder ./laptop --state ./laptop-state doctor
relay    http://127.0.0.1:2451 - answered in 3 ms
stream   vault - stream 946ece38, epoch 1, 7 entries
cursor   at 7 - up to date
key      4f9947f501e49908 - locked 2026-10-09 (today), argon2id 64 MiB × 3 × 4
device   28263325
epoch    1 - written, and read up to

What the relay's operator sees is positions, sizes and links - the first entry is the stream's key, locked under the passphrase; the rest is ciphertext:

$ efema-server streams vault --data ./relay
SEQ  EPOCH      SIZE  HASH
  1  1         172 B  6185a8ef
  2  1         141 B  8f124ce6
  3  1         132 B  16651889
  4  1      47.0 KiB  2a0aad78
  5  1         185 B  aceb0973
  6  1         171 B  2092dd63
  7  1         173 B  3a5190d8

What you get

  • Sealed on the device. A client cannot be made without the stream's passphrase or key, and has no way to send an entry unsealed. The relay cannot read, forge or alter an entry, nor tell which device wrote it.
  • One order for everyone. Batches land whole, on consecutive positions, and every device reads the same sequence however many wrote at once.
  • Cursors that cannot lie. After a restore from an old backup, a device is told its copy and the relay's have parted - and never writes into a stream that is not the one it knew.
  • Epochs. An app that changed its format moves the stream on; older versions stop in front of the first newer entry instead of misreading it.
  • Delivery that survives a crash. A device's cursor moves only when the app says it applied what it read, and survives restarts.
  • One doctor for every app built on it.

Install

The relay:

$ cargo install efema-server

Or download the archive for your platform - Linux on x86_64 and ARM64, Windows, macOS on Apple silicon - from the latest release.

The client, in a Rust app:

$ cargo add efema

Then Getting started for the relay, and Syncing an app for the client.

Documentation

The model, sealing, the threat model, every command and the protocol: lacodda.github.io/efema. The API: docs.rs/efema.

Status

0.3.0 is the relay, the client library - entries compressed and sealed on the device, a passphrase that changes without sealing anything again - and lacodda-seal, the sealing on its own. That the relay keeps nothing readable is shown by tests, listed in the threat model. The relay has no access control and no TLS of its own yet: run it on loopback or a network you trust, behind a proxy. Changes: CHANGELOG.

Contributing

The gate, the layout and how commits are written: CONTRIBUTING.md.

License

MIT (c) Kirill Lakhtachev