
Resilient data replication for systems that cannot assume connectivity.
Works over 2G, satellite, or no internet for hours • No dropped events. Ever.

# Node A: start listening
zamsync serve ./a 0.0.0.0:7000
# Node B: write an event and sync
zamsync submit ./b '{"patient": "P-001", "type": "visit"}'
zamsync sync ./b 127.0.0.1:7000 $(cat ./a/.node_id)
[node-b] handshake ok peer=a3f2c1d8
[node-b] sent 1 event
[node-b] sync complete in 12ms
Connection dropped mid-transfer? Run sync again. Only missing events are retransmitted.
No Rust, no Cargo. One binary, zero runtime dependencies.
curl -fsSL -o zamsync \
https://github.com/Etoile-Bleu/ZamSync/releases/latest/download/zamsync-linux-x86_64
chmod +x zamsync && sudo mv zamsync /usr/local/bin/
Built for environments where connectivity is a privilege, not a guarantee.
District clinics in rural Bhutan sync patient records to a central hospital hub over 2G cellular -- 600ms latency, 30 kbps bandwidth, and frequent power outages. ZamSync was designed to work correctly through:
Single static binary. Runs on a Raspberry Pi 2. 512 MB RAM minimum.
Long-term vision: enable adoption in rural public health systems such as Bhutan's clinic network, where offline-first synchronization is not an optimization -- it is a requirement.
Want the full story behind the design decisions? Read the deep-dive article on dev.to.
| Category | Feature |
|---|---|
| Core | Append-only WAL with CRC32 integrity and crash recovery |
| Replication | Version Vector deduplication -- no duplicate events ever |
| Ordering | Hybrid Logical Clocks (HLC) for total deterministic ordering |
| Security | Mutual TLS (mTLS) with a hub-signed PKI -- rogue nodes rejected at handshake |
| Encryption | WAL encryption at rest (ChaCha20-Poly1305), per-record random nonce |
| Access control | --policy own -- clinic A cannot read clinic B's records |
| Validation | JSON schema enforcement on submit and replicated events |
| Observability | Prometheus metrics, structured tracing, audit trail (JSON Lines) |
| Operations | WAL compaction, key rotation, daemon mode, benchmarking, event retention (expire, snapshot) |
| Efficiency | Zstd level-3 compression on all frames, chunked batches (256 events/frame) |
| Projection | zamsync project -- project WAL events to SQLite; idempotent INSERT OR IGNORE on (origin_node_id, seq) |
| REST API | Embedded HTTP server (--http) -- POST /submit, GET /events, GET /events/stream (SSE) |
| Platforms | x86_64-linux, aarch64-linux, armv7-linux, x86_64-windows (static musl binaries) |
ZamSync uses hexagonal architecture -- the sync core is pure logic with no I/O. Storage and transport are pluggable adapters.
+----------------------------------------------------------+
| zamsync (CLI binary) |
+--------------------+--------------------+-----------------+
| |
+------------+-------+ +---------+----------+ +------------------+
| zamsync-core | | zamsync-storage | | zamsync-network |
| | | | | |
| Event, HLC | | ZamEngine | | TcpTransport |
| VersionVector | | WalEventStore | | TlsTcpTransport |
| SyncMessage | | FilePeerStore | | wire protocol |
| port traits | | WAL (CRC32) | | mTLS (rustls) |
+--------------------+ +--------------------+ +------------------+
Clinic (initiator) Hospital (responder)
| |
|-- Handshake(node_id, vv) ------->|
| | compare VVs, find gaps
|<-- EventBatch(events...) --------|
|<-- EventBatch(events...) --------| chunked, 256 events/frame
|<-- SyncComplete -----------------|
| |
|-- EventBatch(my_events...) ----->|
|-- SyncComplete ----------------->|
| |
Events are idempotent -- if a session is interrupted mid-transfer, the next sync only sends what's missing.
Static musl binaries -- work on any Linux, no libc version requirements.
# Linux x86_64
curl -fsSL -o zamsync \
https://github.com/Etoile-Bleu/ZamSync/releases/latest/download/zamsync-linux-x86_64
chmod +x zamsync && sudo mv zamsync /usr/local/bin/
# Linux ARM64 (Raspberry Pi 4, AWS Graviton)
curl -fsSL -o zamsync \
https://github.com/Etoile-Bleu/ZamSync/releases/latest/download/zamsync-linux-aarch64
chmod +x zamsync && sudo mv zamsync /usr/local/bin/
# Linux ARMv7 (Raspberry Pi 2 / 3)
curl -fsSL -o zamsync \
https://github.com/Etoile-Bleu/ZamSync/releases/latest/download/zamsync-linux-armv7
chmod +x zamsync && sudo mv zamsync /usr/local/bin/
# Windows (PowerShell)
Invoke-WebRequest `
-Uri "https://github.com/Etoile-Bleu/ZamSync/releases/latest/download/zamsync-windows-x86_64.exe" `
-OutFile zamsync.exe
Verify:
sha256sum -c SHA256SUMS.txt
zamsync info ./test-node
# Node ID: a3f2c1d8e5b6... events: 0 peers: 0
docker pull ghcr.io/etoile-bleu/zamsync:latest
docker run --rm \
-v /var/lib/zamsync:/data \
-p 7000:7000 \
ghcr.io/etoile-bleu/zamsync:latest \
serve /data 0.0.0.0:7000
git clone https://github.com/Etoile-Bleu/ZamSync.git
cd ZamSync
cargo build --release
# binary at: target/release/zamsync
zamsync submit ./node '{"patient": "P-001", "ward": "3B", "type": "admission"}'
zamsync submit ./node '{"patient": "P-001", "type": "discharge"}'
zamsync submit ./node '{"patient": "P-002", "ward": "ICU", "type": "admission"}'
zamsync info ./node
node_id : 2748582051
data_dir : ./node
events : 3
vv : node 2748582051 @ seq 3
wal size : 1 KB
oldest : 2026-06-17
newest : 2026-06-17
zamsync audit ./node --format json | jq '{seq: .seq, type: .payload_type, size: .payload_size}'
{"seq": 1, "type": "json", "size": 54}
{"seq": 2, "type": "json", "size": 36}
{"seq": 3, "type": "json", "size": 44}
# Terminal 1 -- hub node listens
zamsync serve ./hub 0.0.0.0:7000
# Terminal 2 -- clinic submits and syncs
zamsync submit ./clinic '{"patient": "P-042", "type": "visit"}'
zamsync sync ./clinic 127.0.0.1:7000 $(cat ./hub/.node_id)
[clinic] connecting to 127.0.0.1:7000...
[clinic] handshake ok peer=hub
[clinic] sent 1 event
[clinic] received 0 events
[clinic] sync complete in 8ms
# Hub now has the clinic's event
zamsync info ./hub
node_id : 3001442771
data_dir : ./hub
events : 1
vv : node 1482937654 @ seq 1
wal size : 1 KB
oldest : 2026-06-17
newest : 2026-06-17
# Generate a node with a WAL encryption key
zamsync keygen ./secure-node
# All writes are encrypted at rest
zamsync submit ./secure-node '{"sensitive": "data"}' \
--key-file ./secure-node/data.key
# Audit requires the key
zamsync audit ./secure-node --key-file ./secure-node/data.key
2026-06-14T12:00:01Z seq=1 node=a3f2c1d8 size=22B sha256=4a8f2c...
zamsync project reads the local WAL and inserts every event into a SQLite database via INSERT OR IGNORE, making your sync data queryable with standard SQL tools. Re-running is always safe: already-present rows are skipped via a UNIQUE(origin_node_id, seq) constraint.
# Basic: project all events into projection.db (default path)
zamsync project ./node
# Custom output path
zamsync project ./node --target sqlite://./data/events.db
# Dry run: preview what would be projected without writing anything
zamsync project ./node --dry-run
# Tune batch size for large WALs (default: 100)
zamsync project ./node --batch-size 500
3 projected, 0 already present
Query the database directly once projected:
sqlite3 ./node/projection.db \
"SELECT origin_node_id, seq, hlc_ms, payload FROM zamsync_events ORDER BY hlc_ms;"
import sqlite3, json
conn = sqlite3.connect("./node/projection.db")
for row in conn.execute("SELECT seq, payload FROM zamsync_events ORDER BY hlc_ms"):
print(row[0], json.loads(row[1]))
Schema:
| Column | Type | Description |
|---|---|---|
origin_node_id |
INTEGER | Node that originally submitted the event |
seq |
INTEGER | Sequence number on the origin node |
hlc_ms |
INTEGER | HLC wall-clock component (Unix ms) |
hlc_logical |
INTEGER | HLC logical counter (tie-breaker) |
event_type |
INTEGER | Application-defined event type |
payload |
BLOB | Raw event payload (JSON or binary) |
projected_at |
TEXT | ISO 8601 timestamp of when this row was inserted |
# Clinic node syncs automatically every 5 minutes
zamsync daemon ./clinic 192.168.1.10:7000 $(cat ./hub/.node_id) \
--tls \
--key-file ./clinic/data.key \
--interval 300 \
--metrics 0.0.0.0:9090
[daemon] starting peer=192.168.1.10:7000 interval=300s
[daemon] sync #1 sent=3 received=12 duration=1.2s
[daemon] sleeping 300s...
[daemon] sync #2 sent=0 received=7 duration=0.8s
+--------------+
| Hub / CA |
| (hospital) |
+------+-------+
signs | signs
+----------------+--------------+
| |
+------+------+ +---------+---+
| Clinic A | | Clinic B |
| node.crt | | node.crt |
+-------------+ +-------------+
All share the same ca.crt -- mTLS enforced
# 1. Initialize the hub (once)
zamsync keygen /var/lib/hub
# 2. Sign a certificate for each clinic
zamsync sign /var/lib/clinic-a --ca /var/lib/hub
zamsync sign /var/lib/clinic-b --ca /var/lib/hub
# 3. Copy clinic directories to physical devices
scp -r /var/lib/clinic-a pi@clinic-a.local:/var/lib/zamsync
# 4. Hub serves with mTLS, per-clinic isolation, and concurrent connections
zamsync serve /var/lib/hub 0.0.0.0:7000 --tls --policy own --max-peers 32
# 5. Clinics sync automatically
zamsync daemon /var/lib/zamsync 192.168.1.10:7000 $(cat /var/lib/hub/.node_id) \
--tls --key-file /var/lib/zamsync/data.key --interval 300
A rogue node that presents a certificate from a different CA is rejected at the TLS handshake before any data is exchanged.

The WAL is encrypted at rest using ChaCha20-Poly1305 AEAD with a random 96-bit nonce per record. All commands accept --key-file:
zamsync submit ./node "event" --key-file ./node/data.key
zamsync serve ./node 0.0.0.0:7000 --key-file ./node/data.key
zamsync sync ./node 192.168.1.10:7000 <peer-id> --key-file ./node/data.key
zamsync audit ./node --key-file ./node/data.key
zamsync compact ./node --key-file ./node/data.key
zamsync rekey ./node --old-key ./node/data.key --new-key ./node/data.key.new
mv ./node/data.key.new ./node/data.key

--policy own on serve enforces per-tenant isolation server-side:
Clinic A submits: A1, A2, A3
Clinic B submits: B1, B2
When clinic A syncs -> receives A1, A2, A3 (not B1, B2)
When clinic B syncs -> receives B1, B2 (not A1, A2, A3)
Hub sees all events.
![Access control d
$ claude mcp add ZamSync \
-- python -m otcore.mcp_server <graph>