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
doctorfor 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