bothy
Serverless, local-first sync engine that uses the user's own cloud storage as the transport.
⚠️ Early release (0.2). The API and the on-remote format can still change before 1.0.
Each device keeps a local SQLite database as the source of truth and works offline. Devices exchange immutable files through the user's own Google Drive (or a shared directory). There is no backend and no account of ours.
Example
use Arc;
use ;
use json;
let remote = new;
let db = open.await?;
db.init_or_join.await?;
let notes = db.collection?;
notes.put.await?; // whole document
notes.patch.await?; // some fields
let note = notes.get.await?;
let report = db.sync.await?; // push local changes, pull remote ones
let mut events = db.subscribe; // local and remote change events
let auto = db.start_auto_sync; // inside an async context
For Google Drive, implement bothy::auth::AccessTokenProvider (OAuth stays in your app) and
use remote::drive::DriveRemote::appdata(provider) for the hidden app folder (scope
drive.appdata), or DriveRemote::folder(provider, "notes") for a visible Bothy/notes
folder (scope drive.file, also reachable from the device sign-in flow of headless boards).
Encryption
// First device: creates the database encrypted with this password.
let db = open.await?;
// Other devices: join (or unlock on open) with the same password.
let db = open.await?;
// Keep the data key in secure storage to skip the password next time.
let key = db.export_key?;
let db = open.await?;
Segment bodies, snapshot chunks and device.json are encrypted with XChaCha20-Poly1305 under
a random data key, which per-device key files wrap with an Argon2id key derived from the
password.
- Wrong secret: a wrong password fails with
ErrorKind::WrongPassword. - Locked database: opened without a secret, an encrypted database is locked. Local reads
and writes still work;
syncfails withKeyRequireduntilunlock. - Password change:
change_passwordtakes effect on every device, and kept data keys stay valid. - What stays visible: file names, sizes, times and the app id. A forgotten password cannot be recovered.
How it merges
- Every change gets a hybrid logical clock stamp. Fields merge separately, and the larger
(hlc, device)stamp wins. - Edits to different fields of the same document both survive. On the same field the later
write wins;
Database::conflicts(andCollection::conflictsper document) return the losing value.Config::conflict_logsets how many records are kept, or turns the log off. - Deletes keep a tombstone. An edit made before the delete never revives the document; an edit made after it does, with only the new fields.
- Nested objects and arrays are one value. Model concurrent lists as separate documents.
Feature flags
| Feature | Default | What it adds |
|---|---|---|
drive |
yes | remote::drive::DriveRemote (reqwest over rustls with the ring provider; no OpenSSL) |
blocking |
yes | bothy::blocking, a synchronous API with its own runtime |
crypto |
yes | encrypted databases (Config::encrypt/password/key) |
test-util |
no | remote::faulty::FaultyRemote for fault-injection tests |
Links
- Repository and roadmap: https://github.com/bothy-sync/bothy
- CLI:
bothy-cli - Conformance vectors for other implementations:
spec/vectors
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.