Overview
Sync normally means a backend: a server you run and pay for, and a database that holds every user's data in the clear.
coven syncs without the server. Devices exchange end-to-end-encrypted changes through storage the user already has, and merge them locally.
The data is SQLite. You keep your schema; coven owns the connections and runs your queries through them, so it can capture each change with SQLite's session extension, encrypt and sign it, move it through the user's storage, and apply remote changes back.
The round trip
No server is needed because nothing in the loop below requires one. A write is captured, sealed, and parked in storage; every other device picks it up from there. The storage never has to understand the data, which is what lets it be storage the user already has.
The provider only ever holds ciphertext. It never sees a todo title, a file, or who is allowed to write.
In your code
The integration is one builder call and a handful of methods on the handle it returns. Two beats give the flavor; the whole tour, from open to invite, is the Example.
Open the store. Declare the tables that sync and the migration ladder that builds your schema; open installs or verifies Coven's one current internal schema, runs your ladder, and returns one handle. Tables you don't list stay local to the device.
use coven::{Coven, Migration, RowIdentity, SyncedTable, WritePolicy};
let handle = Coven::builder(config)
.write_policy(WritePolicy::MergeConcurrent)
.synced_tables(vec![
SyncedTable::new("todos", RowIdentity::IndependentUuid),
SyncedTable::new("todo_attachments", RowIdentity::IndependentUuid),
])
.migrations(vec![Migration::sql(1, "initial", MY_SCHEMA)])
.open()?;The identity argument states what equal ids mean across devices. Use IndependentUuid with canonical UUIDv4 or UUIDv7 ids for independently created rows. Use SharedKey only when equal application keys intentionally name and merge as one logical row. Changing a primary key removes the old identity and inserts the new validated identity atomically.
Write normally, through the handle. Your closure gets a transaction; coven captures what changed when it commits. Synced rows carry an _updated_at you mint with sql.stamp(); that stamp is how edits order across devices.
let id = uuid::Uuid::new_v4().to_string();
let receipt = handle.sql(move |sql| {
sql.execute(
"INSERT INTO todos (id, title, _updated_at) VALUES (?1, ?2, ?3)",
coven::rusqlite::params![id, title, sql.stamp()],
)?;
Ok(())
}).await?;The returned WriteReceipt names this transaction in coven's durable write ledger. Its initial status is LocalOnly when the transaction changed no shared rows, otherwise Pending. Separate calls produce separate write ids and Store commits.
Read on the read connection. Pure reads go through handle.sql_read, which runs on a read-only companion connection: no change capture, and reads run concurrently with the writer instead of queuing behind it. The connection is read-only at the SQLite layer, so a write inside the closure is refused. A read issued after an awaited write sees that write.
let titles: Vec<String> = handle.sql_read(|conn| {
let mut stmt = conn.prepare("SELECT title FROM todos ORDER BY _updated_at")?;
let rows = stmt.query_map([], |row| row.get(0))?;
Ok(rows.collect::<Result<_, _>>()?)
}).await?;Everything else follows the same ownership boundary: handle.write commits a row and its file bytes in one transaction, handle.pending_writes reconstructs unpublished writes after restart, handle.connect_sync starts the background loop, handle.subscribe_sync_status exposes its current state, and handle.invite_member adds a teammate.
Who owns what
The integration stays small because the boundary is strict: coven owns what sync needs to be correct, and the host owns the product.
coven owns the sync layer and the database connections:
- The SQLite connections: one writer, where coven runs the change-capture session and keeps its own bookkeeping, and a read-only companion for queries. You run your writes through the first and your reads through the second; there is no connection coven does not own.
- Capturing local changes and applying remote ones.
- Encrypting, signing, and verifying everything that leaves the device.
- Moving rows and files through your storage.
- Membership, invites, and recovery codes.
You own the app:
- Your schema and your queries, run through coven's connections.
- Which tables sync and which stay local.
- Where user-provided blob files live on disk.
- Provider configuration and credentials.
- All UI and product policy.
Topics
In reading order; each page builds on the ones before it:
- Local data: the store on one device: schema conventions, which tables sync, which rows share.
- Sync: change capture, the cycle, push and pull.
- Merge: the clock, and how concurrent edits land on every device.
- Storage: the
CloudHomecontract and the providers. - Blobs: files that rows carry, where their bytes live, and how they move.
- Cache: the device-local copies of remote files: budgets, pinning, eviction.
- Sharing: membership, roles, invite, join, revoke.
- Bootstrap: snapshots and how a new device joins or restores.
- Encryption: the keys, what is encrypted, what the provider sees.
- Keys: where each key lives on the device, the custody presets, and what a host has to set up per platform.
- Schema evolution: migrating the synced schema while devices upgrade at different times.
Status
coven is pre-1.0.