Repliq is a local-first, CRDT-based data sync engine for Rust. Build apps that work offline, sync automatically between devices, and merge data without conflicts.
- Conflict-Free Replication: Built on proven CRDT (Conflict-free Replicated Data Type) algorithms
- Offline-First: All data is stored locally; sync happens in the background
- Multiple CRDT Types: LWW-Set, LWW-Map (more coming soon)
- Persistent Storage: Local data persistence using sled (embedded database)
- Network Sync: WebSocket-based sync layer for multi-device support
- Rust-Native: Fast, safe, and memory-efficient
repliq/
├── repliq-core/ # Core CRDT implementations
├── repliq-storage/ # Local persistence layer (sled)
├── repliq-network/ # WebSocket sync protocol
├── repliq-cli/ # CLI tool for running sync servers
└── examples/
└── notes-app/ # Example notes application
Add Repliq to your Cargo.toml:
[dependencies]
repliq-core = { path = "path/to/repliq/repliq-core" }
repliq-storage = { path = "path/to/repliq/repliq-storage" }
repliq-network = { path = "path/to/repliq/repliq-network" }cargo build --releaseAdd a note:
cargo run -p notes-app -- add "My First Note" "This is the content"List all notes:
cargo run -p notes-app -- listStart a sync server:
cargo run -p notes-app -- server --addr 127.0.0.1:8080Sync with server (in another terminal):
cargo run -p notes-app -- --data-dir ./data2 sync --url ws://127.0.0.1:8080use repliq_core::{LwwMap, CrdtState, ReplicaId};
// Create a new map with a unique replica ID
let replica_id = ReplicaId::new();
let mut map = LwwMap::new(replica_id);
// Add/update entries
map.set("user_name".to_string(), "Alice".to_string());
map.set("user_age".to_string(), "30".to_string());
// Get values
if let Some(name) = map.get(&"user_name".to_string()) {
println!("Name: {}", name);
}
// Merge with another replica
let mut map2 = LwwMap::new(ReplicaId::new());
map2.set("user_email".to_string(), "alice@example.com".to_string());
map.merge(map2);
println!("Total entries: {}", map.len());use repliq_core::{LwwSet, CrdtState, ReplicaId};
let replica_id = ReplicaId::new();
let mut set = LwwSet::new(replica_id);
// Add elements
set.add("apple".to_string());
set.add("banana".to_string());
// Check membership
assert!(set.contains(&"apple".to_string()));
// Remove elements
set.remove("apple".to_string());
assert!(!set.contains(&"apple".to_string()));
// Get all elements
let elements = set.elements();
println!("Set contains: {:?}", elements);use repliq_storage::{SledStorage, Storage};
use repliq_core::LwwMap;
// Open storage
let backend = SledStorage::open("./data")?;
let storage = Storage::new(backend);
// Store CRDT state
let map = LwwMap::new(replica_id);
storage.put("my_map", &map)?;
// Load CRDT state
let loaded_map: Option<LwwMap<String, String>> = storage.get("my_map")?;use repliq_network::Server;
#[tokio::main]
async fn main() -> Result<()> {
let server = Server::new("127.0.0.1:8080".to_string());
server.run().await?;
Ok(())
}use repliq_network::{Client, Message};
use repliq_core::ReplicaId;
#[tokio::main]
async fn main() -> Result<()> {
let replica_id = ReplicaId::new();
let mut client = Client::new(replica_id);
client.connect("ws://127.0.0.1:8080").await?;
client.run(|message| {
match message {
Message::Sync(sync_msg) => {
println!("Received sync: {:?}", sync_msg);
}
_ => {}
}
}).await?;
Ok(())
}LWW-Map (Last-Write-Wins Map)
- Key-value store where the last write (by timestamp) wins
- Uses hybrid logical clocks for ordering
- Handles concurrent updates gracefully
LWW-Set (Last-Write-Wins Set)
- Set data structure with add/remove operations
- Elements are never truly deleted, just marked as removed
- Bias towards additions when timestamps are equal
Repliq uses Hybrid Logical Clocks (HLC) that combine:
- Physical time (milliseconds since epoch)
- Logical counter (for events at the same physical time)
This ensures total ordering of events across replicas without requiring clock synchronization.
Conflicts are resolved deterministically using:
- Timestamp comparison: Later timestamp wins
- Replica ID tie-breaking: Higher replica ID wins if timestamps are equal
- Local Operations: All changes are applied locally first and persisted
- Operation Broadcast: When connected, operations are sent to peers via WebSocket
- Merge on Receive: Incoming operations are merged using CRDT semantics
- Eventual Consistency: All replicas converge to the same state
Device A Server Device B
| | |
|--- Sync(map_state) --->| |
| |--- Broadcast --------->|
| | | [Merge]
| |<--- Sync(map_state) ---|
|<--- Broadcast ---------| |
| [Merge] | |
Instead of syncing full state, only send operations (deltas) since last sync:
// Track vector clocks per peer
// Send only operations newer than peer's last known stateEncrypt local storage for sensitive data:
// Use age or sodiumoxide for encryption
// Encrypt before storing, decrypt after loading- JavaScript/TypeScript: WebAssembly bindings via wasm-bindgen
- Python: PyO3 bindings
- Go: CGO bindings (already used in the workspace)
- CRDT Text: Collaborative text editing (RGA, Automerge-style)
- Counter: Increment/decrement counters
- Registers: Multi-value register (MVR)
Replace WebSocket with libp2p for:
- Peer-to-peer connections (no central server)
- NAT traversal
- Discovery mechanisms
Prune old tombstones and reduce memory usage:
// Keep operations for last N days only
// Compact state periodicallyRun all tests:
cargo test --workspaceRun specific module tests:
cargo test -p repliq-core
cargo test -p repliq-storage
cargo test -p repliq-networkcargo benchContributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure
cargo testandcargo clippypass - Submit a pull request
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
- CRDTs: Consistency without concurrency control
- Hybrid Logical Clocks
- Automerge: A CRDT for collaborative applications
Inspired by: