crdt-kit 0.3.0

CRDTs optimized for edge computing and local-first applications
Documentation

crdt-kit

Conflict-free Replicated Data Types for Rust

Build offline-first, real-time, and distributed applications that just work.

Crates.io Downloads Docs.rs CI License

DocumentationCrateExamplesContributing

Why crdt-kit?

Traditional sync solutions break when devices go offline. CRDTs solve this at the data structure level — every replica can be updated independently, and merges always converge to the same result, guaranteed by math, not by servers.

crdt-kit is built specifically for resource-constrained, latency-sensitive environments where existing solutions (Automerge, Yjs) add too much overhead:

+-----------+     +-----------+     +-----------+
|  Device A |     |  Device B |     |  Device C |
|  (offline)|     |  (offline)|     |  (offline)|
+-----+-----+     +-----+-----+     +-----+-----+
      |                 |                 |
      |  local edits    |  local edits    |  local edits
      |                 |                 |
      +--------+--------+--------+--------+
               |                 |
               v                 v
         +-----------+     +-----------+
         |   merge   |     |   merge   |
         +-----------+     +-----------+
               |                 |
               v                 v
          Same state!       Same state!    <-- Strong Eventual Consistency

Key Advantages

crdt-kit Automerge Yjs
Zero dependencies (core) Yes No (30+) No (N/A — JS)
no_std / embedded Yes No No
WASM-ready Yes Partial Native JS
Persistent storage SQLite, redb, memory Custom N/A
Schema migrations Automatic (lazy) Manual N/A
Delta sync Yes Yes Yes
Serde integration Yes Custom N/A
Pure Rust Yes Yes No (JS)

Built For

Environment Why it matters
IoT / Embedded no_std support — runs on bare metal, Raspberry Pi, ESP32
Mobile apps Offline-first with automatic conflict resolution on reconnect
Edge computing Delta sync minimizes bandwidth between edge nodes
P2P networks No central server needed — every peer is equal
Real-time collaboration Concurrent edits merge without coordination
WASM / Browser First-class WebAssembly bindings for web apps

Quick Start

[dependencies]
crdt-kit = "0.3"
use crdt_kit::prelude::*;

// Two devices, working offline
let mut phone = GCounter::new("phone");
phone.increment();
phone.increment();

let mut laptop = GCounter::new("laptop");
laptop.increment();

// When they reconnect — merge. Always converges.
phone.merge(&laptop);
assert_eq!(phone.value(), 3);

With Persistence

[dependencies]
crdt-kit = { version = "0.3", features = ["serde"] }
crdt-store = { version = "0.1", features = ["sqlite"] }
use crdt_store::{CrdtDb, CrdtVersioned, SqliteStore};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct SensorReading { temperature: f32, humidity: f32 }

impl CrdtVersioned for SensorReading {
    const SCHEMA_VERSION: u8 = 1;
}

let store = SqliteStore::open("sensors.db").unwrap();
let mut db = CrdtDb::with_store(store);

// Save — automatically wrapped in a version envelope
db.save("sensor-42", &SensorReading { temperature: 22.5, humidity: 65.0 }).unwrap();

// Load — automatically migrated if the schema changed
let reading: Option<SensorReading> = db.load("sensor-42").unwrap();

Workspace Architecture

crdt-kit is a multi-crate workspace. Use only what you need:

crdt-kit/
├── crates/
│   ├── crdt-kit/              9 CRDTs + HLC + traits (the core library)
│   ├── crdt-store/            Persistence: SQLite, redb, memory + CrdtDb API
│   ├── crdt-migrate/          Version envelopes + migration engine
│   ├── crdt-migrate-macros/   #[crdt_schema] + #[migration] proc macros
│   ├── crdt-codegen/          Code generation from TOML schemas
│   ├── crdt-cli/              CLI: status/inspect/compact/export/generate/dev-ui
│   ├── crdt-dev-ui/           Embedded web panel for database inspection
│   └── crdt-example-tasks/    Full example: codegen + migrations + events
Crate Description Feature flags
crdt-kit 9 CRDT types, HLC, Crdt/DeltaCrdt traits std, serde, wasm
crdt-store Unified storage abstraction + high-level CrdtDb sqlite, redb
crdt-migrate Transparent schema migrations (lazy, on-read) macros
crdt-codegen Generate structs, migrations, and helpers from TOML
crdt-cli Developer CLI tool (crdt binary)
crdt-dev-ui Web inspection panel (Axum, dark theme)

Persistence & Storage

Three backends, one trait surface:

Backend Feature Use case Dependencies
MemoryStore (always) Testing, prototyping None
SqliteStore sqlite Edge Linux, mobile, desktop rusqlite (bundled)
RedbStore redb Pure-Rust edge (no C deps) redb

All backends implement StateStore + EventStore with event sourcing, snapshots, and compaction:

use crdt_store::{EventStore, MemoryStore, StateStore};

let mut store = MemoryStore::new();

// State persistence
store.put("sensors", "s1", b"data").unwrap();

// Event sourcing
let seq = store.append_event("sensors", "s1", b"SetTemp(23.1)", 1000, "node-a").unwrap();

// Snapshots + compaction
store.save_snapshot("sensors", "s1", b"state", seq, 1).unwrap();
store.truncate_events_before("sensors", "s1", seq).unwrap();

Schema Migrations

When your data evolves between app versions, crdt-migrate handles it transparently:

use crdt_migrate::{crdt_schema, migration};

#[crdt_schema(version = 1, table = "sensors")]
#[derive(Serialize, Deserialize)]
struct SensorV1 { device_id: String, temperature: f32 }

#[crdt_schema(version = 2, table = "sensors")]
#[derive(Serialize, Deserialize)]
struct SensorV2 { device_id: String, temperature: f32, humidity: Option<f32> }

#[migration(from = 1, to = 2)]
fn add_humidity(old: SensorV1) -> SensorV2 {
    SensorV2 { device_id: old.device_id, temperature: old.temperature, humidity: None }
}
  • Lazy: Data migrates on read, not on startup
  • Deterministic: Two devices migrating the same data produce identical results
  • Linear chain: v1 → v2 → v3 → current, never skipping steps
  • Write-back: Migrated data is re-persisted automatically

Code Generation

Define your entities in a crdt-schema.toml file and generate all the Rust boilerplate:

[config]
output = "src/generated"

# Entity with CRDT-wrapped fields (conflict-free replicated)
[[entity]]
name = "Project"
table = "projects"

[[entity.versions]]
version = 1
fields = [
    { name = "name", type = "String", crdt = "LWWRegister" },
    { name = "members", type = "String", crdt = "ORSet" },
]

# Entity with plain fields, versioned schema, and a relation
[[entity]]
name = "Task"
table = "tasks"

[[entity.versions]]
version = 1
fields = [
    { name = "title", type = "String" },
    { name = "done", type = "bool" },
]

[[entity.versions]]
version = 2
fields = [
    { name = "title", type = "String" },
    { name = "done", type = "bool" },
    { name = "priority", type = "Option<u8>", default = "None" },
    { name = "tags", type = "Vec<String>", default = "Vec::new()" },
    { name = "project_id", type = "String", default = "String::new()", relation = "Project" },
]
$ crdt generate --schema crdt-schema.toml
  Generated: src/generated/project.rs
  Generated: src/generated/task.rs
  Generated: src/generated/task_migrations.rs
  Generated: src/generated/helpers.rs
  Generated: src/generated/mod.rs

Generated 5 files in src/generated/

Field options:

Option Example Effect
type "String", "Vec<u8>" Rust type for the field
default "None", "Vec::new()" Default value for migrations
crdt "LWWRegister", "ORSet", "GCounter" Wraps the type with a CRDT (e.g., LWWRegister<String>)
relation "Project" Documents a reference to another entity

Supported CRDT types: GCounter, PNCounter, LWWRegister, MVRegister, GSet, TwoPSet, ORSet

What gets generated:

  • project.rs / task.rs — Versioned structs with #[crdt_schema], CRDT-wrapped field types, and type Project = ProjectV1
  • task_migrations.rs — Migration functions with #[migration] (auto-generated for field additions, CRDT fields get auto-defaults)
  • helpers.rscreate_db() and create_memory_db() with all migrations pre-registered
  • mod.rs — Module declarations and re-exports

All files are marked AUTO-GENERATED by crdt-codegen -- DO NOT EDIT.

See crdt-example-tasks for a complete working example.


Developer Tools

CLI

$ crdt status app.db          # Database overview
$ crdt inspect app.db sensor-42 --events  # Entity detail + event log
$ crdt compact app.db --threshold 100     # Snapshot + truncate events
$ crdt export app.db --namespace sensors  # JSON export
$ crdt generate --schema crdt-schema.toml # Generate code from TOML
$ crdt dev-ui app.db          # Launch web inspector

Dev UI

A dark-themed web panel for visual database inspection during development:

$ crdt dev-ui app.db
  Dev UI: http://localhost:4242

Browse namespaces, entities, event logs, version envelopes, and snapshots — all from your browser.


Edge Computing & IoT

crdt-kit is purpose-built for edge environments. Import it with no_std for bare metal or with serde for network serialization:

# Raspberry Pi / ESP32 / bare metal (no standard library)
[dependencies]
crdt-kit = { version = "0.3", default-features = false }

# Edge node with JSON sync over MQTT/HTTP
[dependencies]
crdt-kit = { version = "0.3", features = ["serde"] }
serde_json = "1"
use crdt_kit::prelude::*;

// Edge sensor node collects temperature readings
let mut sensor_a = GCounter::new("sensor-a");
let mut sensor_b = GCounter::new("sensor-b");

// Each sensor counts events independently (no network needed)
sensor_a.increment_by(142); // 142 events detected
sensor_b.increment_by(89);  // 89 events detected

// When the gateway collects data — merge. Order doesn't matter.
sensor_a.merge(&sensor_b);
assert_eq!(sensor_a.value(), 231); // exact total, no double-counting

// Delta sync: only send what changed (saves bandwidth on LoRa/BLE)
let mut gateway = GCounter::new("gateway");
let delta = sensor_a.delta(&gateway);  // minimal payload
gateway.apply_delta(&delta);            // gateway is up to date
assert_eq!(gateway.value(), 231);

Available CRDTs

Counters

Type Description Real-world use
GCounter Grow-only counter Page views, IoT sensor events, download counts
PNCounter Increment & decrement Inventory stock, likes/dislikes, seat reservations

Registers

Type Description Real-world use
LWWRegister Last-writer-wins User profile fields, config settings, GPS location
MVRegister Multi-value (shows conflicts) Collaborative fields, version tracking

Sets

Type Description Real-world use
GSet Grow-only set Seen message IDs, tags, audit logs
TwoPSet Add & permanent remove Blocklists, revoked tokens
ORSet Add & remove freely Shopping carts, todo lists, chat members

Sequences

Type Description Real-world use
Rga Replicated Growable Array Playlists, kanban boards, ordered lists
TextCrdt Collaborative text Google Docs-style editing, shared notes

Traits

Trait Description
Crdt Core merge semantics (commutative, associative, idempotent)
DeltaCrdt Efficient delta sync — send only what changed

Examples

# CRDT basics
cargo run -p crdt-kit --example counter         # Distributed counters
cargo run -p crdt-kit --example todo_list        # Collaborative todo list
cargo run -p crdt-kit --example ecommerce        # E-commerce entities
cargo run -p crdt-kit --example chat             # Chat with conflict detection

# Persistence & migration
cargo run -p crdt-store --features sqlite --example iot_sensor      # Schema migration on OTA update
cargo run -p crdt-store --features sqlite --example collaborative   # Multi-node merge + persist
cargo run -p crdt-store --features sqlite --example event_sourcing  # Event log, snapshots, compaction

# Full-stack example (codegen + migrations + event sourcing)
cargo run -p crdt-example-tasks                                     # Task management app

Performance

Measured with Criterion on optimized builds:

Operation Time Throughput
GCounter increment x1000 53 µs ~19M ops/sec
GCounter merge 10 replicas 1.1 µs ~9M merges/sec
GCounter merge 100 replicas 17.8 µs
PNCounter inc+dec x1000 60 µs ~16M ops/sec
ORSet insert x1000 187 µs ~5M ops/sec
ORSet merge 500+500 elements 191 µs
GSet merge 1000+1000 elements 102 µs
LWWRegister merge 100 replicas 11.5 µs ~8M merges/sec
cargo bench  # Run benchmarks yourself

Guarantees

All CRDTs satisfy Strong Eventual Consistency (SEC):

Property Meaning Why it matters
Commutativity merge(a, b) == merge(b, a) Order of sync doesn't matter
Associativity merge(a, merge(b, c)) == merge(merge(a, b), c) Group syncs however you want
Idempotency merge(a, a) == a Safe to retry — no duplicates

Verified by 256 tests across the workspace.


Roadmap

  • G-Counter, PN-Counter
  • LWW-Register, MV-Register
  • G-Set, 2P-Set, OR-Set
  • RGA List (ordered sequence)
  • Text CRDT (collaborative text editing)
  • no_std support (embedded / bare metal)
  • serde serialization support
  • Delta-state optimization
  • WASM bindings
  • Persistent storage (SQLite + redb)
  • High-level CrdtDb API with version envelopes
  • Transparent schema migrations (#[crdt_schema] + #[migration])
  • Developer CLI (crdt status/inspect/compact/export/generate)
  • Dev UI web panel
  • Code generation from TOML schemas (crdt generate)
  • Network transport layer (TCP, WebSocket, QUIC)
  • Sync protocol (delta-based replication)
  • Benchmarks against Automerge / Yrs

Contributing

Contributions are welcome! Please read CONTRIBUTING.md before submitting a pull request.

License

Dual-licensed under your choice of: