Transparent peer-to-peer sync for SeaORM applications.
WaveSyncDB wraps your SeaORM DatabaseConnection and transparently intercepts write operations, replicating them across peers via libp2p gossipsub. Conflicts are resolved automatically using per-column Lamport clocks (CRDTs) — concurrent edits to different columns on the same row both survive, and all nodes converge deterministically. Your application code stays the same — just swap in WaveSyncDb and every insert, update, and delete syncs in the background.
Define a standard SeaORM entity and derive SyncEntity for auto-discovery:
use sea_orm::entity::prelude::*;
use wavesyncdb_derive::SyncEntity;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, SyncEntity)]
#[sea_orm(table_name = "tasks")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub title: String,
pub completed: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}Initialize WaveSyncDB and use it like a normal SeaORM connection:
use sea_orm::*;
use wavesyncdb::WaveSyncDbBuilder;
#[tokio::main]
async fn main() -> Result<(), DbErr> {
// Build the connection — starts the P2P engine automatically
let db = WaveSyncDbBuilder::new("sqlite:./app.db?mode=rwc", "my-app-topic")
.build()
.await?;
// Auto-discover entities annotated with #[derive(SyncEntity)]
db.get_schema_registry(module_path!().split("::").next().unwrap())
.sync()
.await?;
// Standard SeaORM usage — sync happens transparently
let task = task::ActiveModel {
id: Set("1".into()),
title: Set("Buy milk".into()),
completed: Set(false),
..Default::default()
};
task.insert(&db).await?;
Ok(())
}App writes via SeaORM
│
▼
WaveSyncDb (ConnectionTrait wrapper)
│
├──▶ Execute SQL on local database
│
├──▶ Parse SQL → extract column-value pairs
│
├──▶ Update shadow table (per-column Lamport clocks)
│
├──▶ Dispatch SyncChangeset to P2P engine (mpsc channel)
│ │
│ ▼
│ libp2p gossipsub ──▶ Remote peers
│
└──▶ Broadcast ChangeNotification (for reactive UI)
When a remote changeset arrives via gossipsub, the engine:
- Groups column changes by (table, primary key)
- For each column: applies per-column conflict resolution via
should_apply_column()— highercol_versionwins, then value bytes, thensite_id - For deletes: checks
should_apply_delete()with configurableDeletePolicy(DeleteWinsorAddWins) - Applies winning columns to the local database and updates shadow tables
- Broadcasts a
ChangeNotificationso reactive UIs update
| Crate | Description |
|---|---|
| wavesyncdb | Core library: SeaORM connection wrapper, per-column CRDT sync engine, shadow tables, version vector catch-up |
| wavesyncdb_derive | Proc macro: #[derive(SyncEntity)] for auto-discovery of entities |
| wavesync_relay | Relay + rendezvous server for WAN peer discovery and NAT traversal |
The wavesyncdb crate provides Dioxus reactive hooks behind the dioxus feature flag:
use dioxus::prelude::*;
use wavesyncdb::dioxus::{use_wavesync_init, use_synced_table, use_wavesync_provider};
fn main() {
dioxus::launch(App);
}
fn App() -> Element {
use_wavesync_init("sqlite:./app.db?mode=rwc", "my-topic", |db| async move {
db.get_schema_registry(module_path!().split("::").next().unwrap())
.sync()
.await?;
Ok(())
});
let db = use_wavesync_provider();
let tasks = use_synced_table::<task::Entity>(&db);
rsx! {
for task in tasks.read().iter() {
p { "{task.title}" }
}
}
}use_wavesync_init()— creates theWaveSyncDb, runs setup, provides DB via Dioxus contextuse_wavesync_provider()— retrieves theWaveSyncDbfrom contextuse_synced_table::<E>(db)— reactive signal of all rows, auto-refreshes on local and remote changesuse_synced_row::<E>(db, pk)— reactive signal for a single row by primary key
| Type | Description |
|---|---|
WaveSyncDb |
SeaORM ConnectionTrait wrapper that intercepts writes and dispatches sync |
WaveSyncDbBuilder |
Builder to configure and create a WaveSyncDb instance |
SchemaBuilder |
Fluent API for registering entities (.register(), .register_local(), .sync()) |
TableMeta |
Metadata for a synced table (name, primary key, columns, delete policy) |
SyncChangeset |
A set of column-level changes with per-column Lamport clocks and site IDs |
ColumnChange |
A single column change: table, pk, column, value, col_version, site_id |
ChangeNotification |
Lightweight notification emitted after every local or remote write |
DeletePolicy |
Per-table policy: DeleteWins (default) or AddWins |
WriteKind |
Enum: Insert, Update, Delete |
Stable:
- SeaORM connection wrapper with transparent write interception
- P2P sync via libp2p gossipsub with mDNS peer discovery
- Per-column CRDT conflict resolution with Lamport clocks
- Shadow tables (
_wavesync_{table}_clock) for per-column metadata - Version vector sync for catch-up on peer join/reconnect
#[derive(SyncEntity)]for auto-discovery- Dioxus reactive hooks (feature-gated)
- PSK-based peer group authentication (passphrase → BLAKE3 topic isolation + HMAC)
Work in Progress:
- WAN relay + rendezvous server (
wavesync_relay) - NAT traversal (relay/dcutr/autonat behaviours integrated)
- Push notifications for mobile wake-up
WaveSyncDB is dual-licensed:
- AGPL-3.0-or-later — free to use in projects that are themselves AGPL-compatible open-source. AGPL extends copyleft to network use: if you run a modified WaveSyncDB (or its relay) as part of a service, you must offer remote users the source.
- Commercial license — for proprietary, closed-source, or SaaS use where AGPL's source-disclosure requirements are not acceptable.
Pick whichever fits your project. See LICENSE for the umbrella description, LICENSE-AGPL for the open-source terms, and LICENSE-COMMERCIAL for how to obtain a commercial license.
For commercial licensing inquiries: pablo13vazquez@gmail.com.