Skip to content

Latest commit

Β 

History

96 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🌌 VaultSync

CI Status License crates.io npm WASM

Conflict-free, end-to-end encrypted, infrastructure-agnostic synchronization for the local-first era.

VaultSync is an embedded local-first synchronization and replication runtime with CRDT-native conflict resolution, end-to-end encryption, multi-tab/process safety, and zero-trust coordinator architecture. It makes your application work instantly offline, sync automatically when online, and feel 0ms fast β€” because every read and write hits a local database, never a remote server.


Table of Contents


πŸš€ Why VaultSync?

The traditional web architecture forces every user interaction through a server:

User clicks button
     ↓
HTTP Request β†’ API Server β†’ Remote Database
     ↓
Wait 100–2000ms for round-trip
     ↓
UI updates

This creates a cascade of real problems:

  • Your app is unusable offline. No server = no data. A 2-second underground subway trip breaks your product.
  • Every interaction has latency. Every click waits for a network round-trip. On mobile under 4G, that's 200–2000ms.
  • High server costs. Every read and write is an API call. 1 million users = 1 million round-trips per page load.
  • Realtime collaboration is complex. You need WebSockets, event buses, conflict resolution logic β€” all custom.
  • Server outages break everything. When your backend goes down, every user sees an error screen.
  • Data breaches expose everything. If your server is compromised, all user data is exposed in plaintext.

πŸ’‘ The Business Case β€” Real Benefits

VaultSync flips the model: data lives on the device first, syncs to the coordinator in the background.

⚑ Feel 0ms Fast β€” Instant Reads and Writes

Every read and write goes to a local embedded database (SQLite on native, OPFS or IndexedDB in-browser). There is no network round-trip on the critical path.

User clicks button
     ↓
Local CRDT write β†’ SQLite/OPFS  ← sub-millisecond
     ↓
UI updates instantly
     ↓ (background, async)
Encrypted mutation uploads to coordinator
     ↓
Other devices receive & merge

Perceived latency: 0ms. Users feel no lag even on slow networks, because the UI updates before the network is involved.

πŸ“΄ Work Fully Offline β€” Forever

Users can read and write all day on an airplane with no internet. Every mutation is stored locally as an encrypted CRDT entry. When connectivity returns, VaultSync automatically uploads everything and downloads everything they missed β€” no manual conflict resolution, no data loss.

A user who makes 47 changes offline lands, reconnects, and every change syncs without them lifting a finger.

πŸ’Έ Dramatically Lower Server Costs

With VaultSync, reads no longer touch your API servers. A user browsing through 1000 records creates 0 server requests β€” all data is served from local storage. Server load drops to only sync operations (uploads and downloads of CRDT mutations), which are batched and efficient.

In real applications, this reduces API call volume by 60–95%, cutting infrastructure costs proportionally.

🀝 Real-Time Collaboration β€” Built-In

When two users edit the same record at the same time, VaultSync's CRDT merge algorithm handles it automatically and deterministically. There are no conflicts to resolve, no "someone else changed this" dialogs, no last-write-wins data loss. Both changes are preserved.

πŸ”’ Zero-Trust Security β€” Built-In E2EE

The sync server (coordinator) never sees your data. Every CRDT mutation is encrypted on-device with X25519 + ChaCha20-Poly1305 before leaving. The coordinator stores only encrypted blobs. Even if your sync infrastructure is breached, no user data is exposed.

This means VaultSync is HIPAA-ready by default β€” the coordinator literally cannot read patient records.

πŸ—‚οΈ Multi-Tab Safety β€” Built-In

Open the same app in 3 browser tabs. Close the one that was writing. VaultSync automatically elects a new leader, replays any in-flight mutations, and continues β€” with no data loss, no corruption, no stale reads.


πŸ”­ How It Works β€” High-Level

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Your Application Process                       β”‚
β”‚                                                                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   Application Layer                                          β”‚  β”‚
β”‚  β”‚   vaultsync.db.todos.insert({ text: "buy milk" })               β”‚  β”‚
β”‚  β”‚   vaultsync.db.todos.subscribe(callback)                         β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                            β”‚                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   VaultSync Core Engine (Rust + WASM)                            β”‚  β”‚
β”‚  β”‚                                                               β”‚  β”‚
β”‚  β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚   β”‚  CRDT Layer (Yrs)  β”‚  β”‚  E2EE Encryption             β”‚  β”‚  β”‚
β”‚  β”‚   β”‚  Conflict-free     β”‚  β”‚  X25519 + ChaCha20Poly1305   β”‚  β”‚  β”‚
β”‚  β”‚   β”‚  merge of all ops  β”‚  β”‚  Coordinator sees 0 plaintextβ”‚  β”‚  β”‚
β”‚  β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β”‚              β”‚                              β”‚                 β”‚  β”‚
β”‚  β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚   β”‚  Multi-Tab IPC Layer                                   β”‚  β”‚  β”‚
β”‚  β”‚   β”‚  Leader Election + Shared Memory + Crash Recovery      β”‚  β”‚  β”‚
β”‚  β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β”‚                            β”‚                                  β”‚  β”‚
β”‚  β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚   β”‚  Oplog & Sync Engine                                   β”‚  β”‚  β”‚
β”‚  β”‚   β”‚  Append-only encrypted CRDT mutation log               β”‚  β”‚  β”‚
β”‚  β”‚   β”‚  Upload queue + retry + batching                       β”‚  β”‚  β”‚
β”‚  β”‚   β”‚  Download queue + replay + CRDT merge                  β”‚  β”‚  β”‚
β”‚  β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β”‚                            β”‚                                  β”‚  β”‚
β”‚  β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚   β”‚  Storage Abstraction                                   β”‚  β”‚  β”‚
β”‚  β”‚   β”‚  SQLite / OPFS / IndexedDB / RocksDB / In-Memory       β”‚  β”‚  β”‚
β”‚  β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β”‚                            β”‚                                  β”‚  β”‚
β”‚  β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚   β”‚  Transport Abstraction                                 β”‚  β”‚  β”‚
β”‚  β”‚   β”‚  BC (BroadcastChannel)  β”‚  WS (WebSocket)  β”‚  P2P (WebRTC)  β”‚  β”‚  β”‚
β”‚  β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                     β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚   Zero-Trust Coordinator (your choice)          β”‚
              β”‚   Postgres β”‚ Redis β”‚ Cloudflare DO β”‚ Custom     β”‚
              β”‚   (only sees encrypted blobs + routing metadata)β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ—οΈ Core Architecture

VaultSync is built on three non-negotiable design principles:

1. CRDT-Native From Day One

Concurrent writes never conflict. There is no V1 (last-write-wins) β†’ V2 (field merge) β†’ V3 (CRDT) upgrade path. Every operation from the first line of code is a Yrs CRDT mutation. All replicas converge deterministically regardless of write order.

CRDT Type Merge Behavior Use Cases
LWW-Register Last-write-wins by hybrid logical clock Text fields, status flags, scalars
PN-Counter Increment/decrement sums across all replicas Likes, votes, inventory counts
OR-Set Add/remove with per-element tombstones Tags, labels, member lists
Yrs Text Sequence CRDT (OT-compatible) Rich text, collaborative documents
Yrs Array Ordered sequence with insert/delete/move To-do lists, ranked items

2. Zero-Trust Coordinator

The sync server is cryptographically blind to your data. The coordinator stores:

{
  "id": "mut_abc123",
  "namespace": "workspace:core",
  "replica_id": "laptop-abc",
  "encrypted": "<base64 ciphertext>",
  "timestamp": 1740000000,
  "sequence": 42
}

No field names. No field values. No schema info. Even if your coordinator is fully compromised, attackers see only encrypted bytes. Decryption keys never leave the device.

3. Infrastructure-Agnostic

The Coordinator is a Rust trait, not a specific service. Choose or build:

pub trait Coordinator: Send + Sync + Debug {
    async fn push(mutations: Vec<EncryptedMutation>) -> Result<Vec<SequenceId>>;
    async fn pull(after: SequenceId, limit: usize) -> Result<Vec<PendingMutation>>;
    async fn subscribe(from_sequence: SequenceId) -> Result<Box<dyn Stream<...>>>;
    async fn register(namespace: &str, info: ReplicaInfo) -> Result<()>;
}

Swap from Postgres to Redis to Cloudflare DO without changing a single line of application code.


πŸ”€ CRDT Document Model

How CRDTs Eliminate Conflicts

Traditional sync engines treat conflicts as exceptional β€” something to detect and resolve after they happen. This leads to:

  • Complex conflict detection logic
  • Data loss on last-write-wins
  • User-facing "merge conflict" dialogs
  • Brittle upgrade paths

CRDTs eliminate the concept of conflict entirely. Every concurrent write produces a deterministic merge.

Replica A edits: doc.set("text", "buy organic milk")   β†’ Yrs update binary diff
Replica B edits: doc.set("completed", true)             β†’ Yrs update binary diff

Both updates applied to either replica:
β†’ { text: "buy organic milk", completed: true }

Both replicas converge. Both changes preserved. No conflicts.

When two replicas edit the same field concurrently:

Replica A (timestamp T1): doc.set("text", "version A")
Replica B (timestamp T2): doc.set("text", "version B")

LWW-Register: latest timestamp wins β†’ { text: "version B" }
Deterministic. No data loss for the later write.

CRDT Document Structure

Every table in VaultSync is a Yrs CRDT Map:

Document: "todos" β†’ record: "todo:123" (Yrs Map)
β”œβ”€β”€ field: "text"       β†’ LWW-Register("build vaultsync")
β”œβ”€β”€ field: "completed"  β†’ LWW-Register(false)
β”œβ”€β”€ field: "priority"   β†’ PN-Counter(3)
β”œβ”€β”€ field: "tags"       β†’ OR-Set(["backend", "rust"])
└── field: "assignee"   β†’ LWW-Register("alice")

Every write produces a Yrs binary diff β€” a small, self-contained update that can be applied to any replica's document to produce the same result, regardless of order.

Why This Means No Breaking Upgrades

Because every operation has always been a CRDT mutation:

  • No data format migration when adding features
  • No conflict handler rewrite ever
  • No schema redefinition as CRDT types are declared at definition time
  • Additive-only schema changes β€” new fields can be added at any time without affecting existing data

πŸ” End-to-End Encryption (E2EE)

Why E2EE Must Be the Default

Every other sync engine (ElectricSQL, Zero, PowerSync, Replicache) stores plaintext data on the coordinator. This means:

  • The coordinator operator can read all synced user data
  • A coordinator breach exposes every user's data
  • HIPAA, GDPR, SOC2 compliance requires additional layers

VaultSync's E2EE ensures the coordinator is zero-trust. It stores only encrypted blobs and routing metadata. It cannot read any application data β€” ever.

Encryption Flow

Local CRDT write
       β”‚
       β–Ό
Yrs binary diff produced (the mutation delta)
       β”‚
       β–Ό
Encrypt with namespace symmetric key:
   ciphertext = ChaCha20Poly1305_encrypt(yrs_update, namespace_key)
       β”‚
       β–Ό
Local oplog stores:
   yrs_update:     plaintext Yrs diff   ← for local fast merge
   encrypted_blob: AEAD ciphertext      ← for upload to coordinator
       β”‚
       β–Ό
Upload to coordinator:
   { id, replicaId, namespace, encrypted_blob, timestamp }
   ← coordinator never sees yrs_update plaintext
       β”‚
       β–Ό
Other replica downloads encrypted_blob
       β”‚
       β–Ό
Decrypt with namespace symmetric key
       β”‚
       β–Ό
Merge Yrs diff into local CRDT document
       β”‚
       β–Ό
UI updates automatically via subscription

Key Architecture

  • X25519 keypair per replica β€” public key registered with coordinator, private key never leaves device
  • Symmetric namespace key β€” shared by all authorized replicas, never sent to coordinator
  • New device onboarding β€” trusted replica wraps the namespace key with the new device's public key and delivers it securely
  • Key rotation β€” supported at any time; old mutations remain decryptable with old key version stored in local keychain
  • Replay attack prevention β€” each mutation has a unique globally-scoped ID; coordinator deduplicates idempotently

What the Coordinator Can and Cannot See

Information Coordinator Sees?
Encrypted mutation blob βœ… (opaque bytes)
Replica ID (who sent it) βœ… (routing)
Namespace (which dataset) βœ… (routing)
Sequence number βœ… (ordering)
Field names ❌ (encrypted)
Field values ❌ (encrypted)
Document structure ❌ (encrypted)
CRDT type information ❌ (encrypted)

πŸ–₯️ Multi-Tab & Multi-Process Safety

The Problem Most Sync Engines Ignore

When multiple browser tabs, Electron windows, or OS processes share the same local database (one SQLite file or OPFS directory), concurrent writes cause:

  • SQLite locking errors (SQLITE_BUSY)
  • CRDT document corruption (concurrent Yrs mutations outside a lock)
  • Oplog ordering violations
  • Subscription double-firing

VaultSync's Solution: Leader Election

VaultSync uses leader election to coordinate write access. One process writes; all others read via shared memory.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Tab A (Leader) β”‚    β”‚   Tab B (Reader)  β”‚    β”‚   Tab C (Reader)  β”‚
β”‚                  β”‚    β”‚                   β”‚    β”‚                   β”‚
β”‚ Write access: βœ…  β”‚    β”‚ Write access: ❌   β”‚    β”‚ Write access: ❌   β”‚
β”‚ Lock: acquired   β”‚    β”‚ Lock: waiting      β”‚    β”‚ Lock: waiting      β”‚
β”‚ Heartbeat: 1s    β”‚    β”‚                   β”‚    β”‚                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                       β”‚                          β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                 β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚    Shared Memory Region      β”‚
                    β”‚  CRDT document snapshots    β”‚
                    β”‚  Oplog tail (ring buffer)   β”‚
                    β”‚  Subscription notifications  β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                 β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚  Local Storage (SQLite/OPFS) β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Leader Election by Platform

Platform Mechanism
Browser (tabs) BroadcastChannel + navigator.locks.request on vaultsync-leader lock
Native (Linux/macOS/Windows) flock() on SQLite WAL file or CreateMutex
Electron Browser mechanism + net.Server on local socket for cross-window IPC

Heartbeat & Crash Recovery

Leader writes heartbeat every 1 second:
  { leaderId: "tab-A", timestamp: ..., sequence: 42 }

Reader checks every 500ms:
  if current_time - leader.heartbeat > 3000ms:
    β†’ leader considered dead
    β†’ reader acquires lock β†’ becomes new leader
    β†’ replays uncommitted ops from shared memory ring buffer
    β†’ notifies other readers via BroadcastChannel

No data is lost β€” shared memory ring buffer preserves last N in-flight mutations.
New leader continues sync from last confirmed sequence.

What Happens Across Tabs in Real Time

When a CRDT merge happens:

  1. Leader merges mutation, writes to storage, updates shared memory
  2. Leader fires subscription callbacks in its own process
  3. Leader sends { changedDocs: ["todos/789"] } via BroadcastChannel
  4. Reader tabs detect change, read from shared memory
  5. Reader tabs fire their own subscription callbacks

All tabs see the update in < 5ms β€” even the ones that didn't initiate the write.


πŸ“΄ Offline Support & Durability

What Happens When You Go Offline

Network disconnects
       β”‚
       β–Ό
VaultSync detects disconnect (WebSocket close / heartbeat timeout)
       β”‚
       β–Ό
Connection status β†’ "disconnected"
       β”‚
       β–Ό
Application continues working fully:
  βœ… Reads served from local CRDT materialized state (instant)
  βœ… Writes produce CRDT mutations β†’ persist to local DB + oplog (status: pending)
  βœ… All mutations E2EE-encrypted before storage
  βœ… Subscriptions continue firing on local CRDT merges
  βœ… UI remains fully interactive
  βœ… Multi-tab: leader continues serving all tabs

Pending mutations accumulate in the local oplog. The coordinator is not involved.

What Happens on Reconnect

Network reconnects
       β”‚
       β–Ό
VaultSync reconnects to coordinator (exponential backoff: 1s β†’ 2s β†’ 4s β†’ ... β†’ 30s)
       β”‚
       β–Ό
Step 1: Upload backlog
  β†’ Read all pending mutations from oplog
  β†’ Sort by local timestamp
  β†’ Upload in batches (encrypted)
       β”‚
       β–Ό
Step 2: Download missed mutations
  β†’ Request all mutations with sequence > last_synced_sequence
  β†’ Decrypt each
  β†’ Merge into local CRDT documents
  β†’ Fire subscriptions for affected documents
       β”‚
       β–Ό
Connection status β†’ "connected"
       β”‚
       β–Ό
Resume real-time sync

Durability Guarantees

Scenario Result
Process crash Mutations in oplog survive β€” re-uploaded on restart
Tab close Shared memory ring buffer preserves in-flight mutations; new leader replays them
Power failure Oplog on persistent storage survives; re-uploaded on restart
Local storage theft E2EE oplog encrypted at rest β€” attacker sees only ciphertext

CRDT uploads are idempotent by mutation ID. Re-uploading a mutation the coordinator has already seen causes a silent deduplicated no-op. Zero duplicates.


πŸ”„ Synchronization Flow

A complete, annotated trace of a local write propagating to a remote replica:

Step 1: Application Write
  vaultsync.db.todos.insert({ id: "todo:123", text: "build vaultsync" })
       β”‚
       β–Ό CRDT Engine
  Yrs document "todos/123" receives mutation
  Yrs produces binary diff (yrs_update)
       β”‚
       β–Ό E2EE Layer
  encrypted_blob = ChaCha20Poly1305_encrypt(yrs_update, namespace_key)
       β”‚
       β–Ό Storage (atomic transaction, Leader only)
  BEGIN TRANSACTION
    INSERT vaultsync_oplog (yrs_update, encrypted_blob, status: "pending")
    UPDATE vaultsync_documents (new Yrs snapshot for fast queries)
  COMMIT
       β”‚
       β–Ό UI
  Subscription fires immediately β€” React re-renders <0.1ms after write

Step 2: Background Upload
  Upload worker reads pending oplog entries
       β”‚
  Batches multiple pending mutations if present
       β”‚
  Sends encrypted_blob to coordinator via WebSocket PUSH frame
  (coordinator never sees yrs_update plaintext)

Step 3: Coordinator Processing
  Validates: replica authorized? schema version compatible?
       β”‚
  Assigns monotonic sequence number (e.g., 44)
       β”‚
  Persists { sequence: 44, replica_id, encrypted_blob, timestamp }
       β”‚
  Fans out MUTATION_PUSH to all other connected replicas in namespace

Step 4: ACK Back to Sender
  Coordinator sends PUSH_ACK { request_id, sequences: [44] }
       β”‚
  Local oplog: sync_status β†’ "synced"
  vaultsync_sync_state: last_synced_sequence = 44

Step 5: Remote Replica Receives Mutation
  Replica B receives MUTATION_PUSH (sequence 44, encrypted_blob)
       β”‚
  Decrypts: yrs_update = decrypt(encrypted_blob, namespace_key)
       β”‚
  CRDT engine: Yrs.merge(yrs_update) into local "todos/123" document
       β”‚
  Subscription fires in Replica B β†’ UI updates
  Total end-to-end: 50–500ms on typical connections

Sync State per Mutation

optimistic β†’ applied locally as optimistic CRDT write, enqueued for upload (OPFS-backed)
pending    β†’ written locally, enqueued for upload
uploading  β†’ currently being sent to coordinator
synced     β†’ coordinator confirmed receipt and assigned sequence
failed     β†’ upload failed; in retry queue (exponential backoff)

Note: There is no "conflict" state. CRDT mutations never conflict.

Hybrid Logical Clock (HLC)

All client timestamps are generated by a CAS-based HybridLogicalClock that ensures monotonic ordering across clock drift and concurrent writes:

now():
  if system_time > stored_wall β†’ jump forward, reset logical counter
  if system_time == stored_wall β†’ increment logical counter
  if system_time < stored_wall (clock went backwards) β†’ increment logical counter

update_with_received(received_hlc):
  local_wall = max(local_wall, received_wall, system_time)
  local_logical = max(local_logical, received_logical) + 1

Key properties:

  • Thread-safe via atomics β€” AtomicU64 for wall clock, AtomicU32 for logical counter, CAS-based update loop.
  • now().wall replaces all previous clientside timestamp generation in the engine.
  • update_with_received() ensures causal ordering when merging mutations from remote peers.
  • Wrapping detection β€” a wrapped flag is set when the logical counter overflows (exposed via hlc_logical_wraps metric).

The clock is stored on VaultSyncClient and drives every local mutation timestamp, including optimistic writes.

WebSocket Wire Protocol

All coordinator communication uses binary WebSocket frames with a 5-byte header:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ type: u8 β”‚ length: u32β”‚ payload: JSON bytes (length bytes)      β”‚
β”‚ (1 byte) β”‚ (4 bytes)  β”‚                                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key message types: AUTH, REGISTER, PUSH, PUSH_ACK, PULL, PULL_RESPONSE, MUTATION_PUSH, HEARTBEAT, KEY_FETCH, SCHEMA_SYNC.

REGISTER_ACK payload now includes a generation_id field (#[serde(default)] for backward compatibility). On register, the client compares this with its stored SyncState.generation_id. A mismatch triggers an eager cursor reset to 0 and full re-download β€” this handles server restart scenarios where the mutation store has been replaced.


πŸ“‘ Reactive Subscriptions

VaultSync subscriptions fire on both local writes and incoming remote CRDT merges:

// Table subscription β€” fires on any CRDT merge in the collection
const unsub = vaultsync.db.todos.subscribe((todos) => {
  renderTodoList(todos)
})

// Filtered subscription
const unsub = vaultsync.db.todos.subscribe(
  (todos) => renderTodos(todos),
  { where: { assignee: "alice", completed: false } }
)

// Single record subscription
const unsub = vaultsync.db.todos.subscribeOne("todo:123", (todo) => {
  if (!todo) renderDeleted()
  else renderTodo(todo)
})

// Sync status subscription
vaultsync.subscribe("sync:status", (status) => {
  updateConnectionIndicator(status)
  // status: { connected, pendingUploads, lastSyncAt, leaderStatus, optimisticWrites, pushMutationsReceived, snapshotsApplied, activePeers, hlcLogicalWraps }
})

React Hooks (via @vaultsync/react)

import { useQuery, useVaultSyncOne, useSyncStatus, useVaultSyncMutations } from "@vaultsync/react"

function TodoList() {
  // Reactive live query β€” re-renders whenever any todo changes locally or remotely
  const { data: todos, loading } = useQuery("todos", {
    where: { completed: false },
    orderBy: { field: "priority", direction: "desc" }
  })

  const { insert } = useVaultSyncMutations("todos")

  return (
    <div>
      <button onClick={() => insert({ text: "new task", completed: false })}>
        Add
      </button>
      {loading ? <Spinner /> : <ul>{todos.map(t => <TodoItem key={t.id} todo={t} />)}</ul>}
    </div>
  )
}

function SyncIndicator() {
  const { connected, pendingUploads } = useSyncStatus()
  const { peerCount } = useSyncStatus({ showPeers: true })
  return (
    <span className={connected ? "synced" : "pending"}>
      {connected ? `βœ“ Synced (${peerCount} peer${peerCount !== 1 ? 's' : ''})` : `⟳ ${pendingUploads} pending`}
    </span>
  )
}

Subscription callbacks fire once per CRDT merge β€” not once per field. A remote mutation that changes 10 fields fires one subscription callback with the full post-merge document state.

Client Metrics & Presence APIs

Access engine telemetry directly from JavaScript:

// Snapshot of all metrics counters and gauges
const snapshot = vaultsync.metrics()
// { mutationsUploaded, mutationsDownloaded, syncErrors, pendingMutations,
//   pushMutationsReceived, snapshotsApplied, optimisticWrites,
//   hlcLogicalWraps, activePeers, ... }

// Presence awareness β€” active peers in the same namespace
const pc = vaultsync.presence()
pc.peerCount()   // number of visible peers
pc.activePeers() // JSON string of { replicaId: lastSeenMs }

The PresenceManager class is exported as a #[wasm_bindgen] constructor, making it directly instantiable from JavaScript as new PresenceManager(namespace, replicaId) for standalone use.


πŸ”Œ Coordinator Abstraction Layer

Why a Trait, Not a Service

Every other sync engine locks you into a specific backend. VaultSync defines Coordinator as a Rust trait. You choose the implementation. You can swap backends without changing application code.

Built-in Implementations

Backend Package Real-Time Best For
PostgreSQL vaultsync-coordinator-postgres LISTEN/NOTIFY (5–50ms) Production self-hosted
Redis vaultsync-coordinator-redis Pub/Sub (1–10ms) High throughput, in-memory
SQLite vaultsync-coordinator-sqlite Polling (250ms) Local dev, single-server
Cloudflare DO vaultsync-coordinator-cloudflare DO WebSocket (10–50ms) Edge-deployed, global
Supabase vaultsync-coordinator-supabase Supabase Realtime (10–100ms) Hosted Postgres
In-Memory vaultsync-coordinator-memory Channel (< 1ms) Testing

Switching Coordinators

// PostgreSQL
const vaultsync = new VaultSync({
  namespace: "workspace:core",
  storage: "opfs",
  coordinator: new PostgresCoordinator({
    connectionString: "postgres://user:pass@host/db"
  })
})

// Cloudflare Durable Objects
const vaultsync = new VaultSync({
  namespace: "workspace:core",
  storage: "opfs",
  coordinator: new CloudflareCoordinator({
    accountId: "...",
    durableObjectId: "..."
  })
})

// Your own implementation β€” implement the trait
class MyCoordinator implements Coordinator {
  async push(mutations) { /* ... */ }
  async pull(after, limit) { /* ... */ }
  async subscribe(fromSequence) { /* ... */ }
}

Coordinator Server β€” Deployable Binary

VaultSync ships a production-ready coordinator server you can self-host:

# Docker with PostgreSQL backend
docker run -p 8080:8080 \
  -e VAULTSYNC_DB_URL=postgres://user:pass@db:5432/vaultsync \
  -e VAULTSYNC_AUTH_JWKS_URL=https://your-auth.example.com/.well-known/jwks.json \
  vaultsyncsync/coordinator:latest

# Or run the binary
vaultsync-server \
  --backend postgres \
  --db-url postgres://user:pass@localhost/vaultsync \
  --port 8080

New Replica Bootstrap

When a new device joins an existing namespace:

1. New device sends REGISTER with last_sequence = 0
2. Coordinator checks: snapshot available?
3a. YES: returns snapshot_url (compressed batch of encrypted mutations)
    β†’ New device downloads, decompresses, decrypts each mutation
    β†’ Applies all Yrs diffs in sequence order to build local CRDT state
    β†’ Fetches remaining mutations since snapshot
3b. NO: Paginates through full mutation history via PULL
4. Normal real-time sync begins

Coordinator snapshots are encrypted compressed mutation batches β€” the coordinator never decrypts to create them.

Transport Abstraction

VaultSync's Transport trait enables multiple communication channels for mutation delivery:

pub trait Transport: Send + Sync + Debug {
    async fn send(&self, namespace: &str, mutation: &EncryptedMutation) -> Result<(), TransportError>;
    async fn incoming(&self, namespace: &str) -> Result<Box<dyn Stream<Item = InboundMutation>>, TransportError>;
    fn is_available(&self) -> bool;
}

Two built-in implementations:

  • BroadcastChannelTransport (BC) β€” same-origin, plaintext, tab-to-tab via BroadcastChannel. Used for local multi-tab peer delivery without coordinator round-trip.
  • WasmWsTransport (WS) β€” cross-device, encrypted via WebSocket. All mutations are E2EE before transmission.

Both are merged into a single self-filtering deduplication stream via TransportRouter:

  • Uses remotes: Vec<Arc<dyn Transport>> (not Option) to support multiple remote transports simultaneously.
  • Each incoming mutation carries a TransportSource tag (BC or WS) for metrics and debugging.
  • Mutations from the local replica are filtered out to prevent echo loops.
  • The merged stream is bridged through internal futures::channel::mpsc::unbounded() channels and wasm_bindgen_futures::spawn_local for WASM-compatible stream merging.

This architecture keeps transport logic decoupled from the sync engine and allows adding future transports (WebRTC, libp2p) without engine changes.

MutationStore β€” OPFS-Backed Durable Queue

MutationStore provides a crash-recovery queue for pending mutations in the WASM environment:

push(entry)     β†’ persist to OPFS as `<seq>-<uuid>.json` file
pop_batch(limit) β†’ load next N mutations for upload
ack(id)          β†’ remove mutation file (synced)
nack(id)         β†’ leave in place for retry

Each pending mutation is a JSON-serialized OplogEntry stored under a _mutations/ directory in OPFS. On page load, the store scans existing files to recover any mutations that were in-flight during a crash. All local writes flow through MutationStore.push() before upload.

Push-Primary β€” Real-Time via Subscribe Bridge

The coordinator subscription stream is bridged into the download worker's select loop through a futures::channel::mpsc::unbounded() channel. When a push mutation arrives:

  1. Coordinator fans out MUTATION_PUSH to connected replicas.
  2. WasmWsCoordinator deserializes the incoming frame, wraps it as a PendingMutation, and sends it via SyncEvents.download_notify.
  3. DownloadQueue.process_push_mutation() decrypts, CRDT-merges, and advances the cursor immediately β€” no poll cycle needed.
  4. The subscribe() function (in client initialize()) spawns a WASM-compatible listener task that forwards each inbound mutation into the mpsc channel.

This gives sub-second end-to-end delivery for real-time collaboration without polling.

PresenceManager β€” Peer Awareness

PresenceManager tracks active replicas in the same namespace via a dedicated BroadcastChannel (vaultsync-presence-{namespace}):

join:      { type: "join", replica_id, namespace, timestamp }
heartbeat: { type: "heartbeat", replica_id, namespace, timestamp }
leave:     { type: "leave", replica_id, namespace }
  • beforeunload hook sends an immediate leave message when a tab closes.
  • **Peer count** exposed via PresenceManager.peer_count()β€” reflected in theactive_peers` metric.
  • Exported via #[wasm_bindgen] β€” callable from JavaScript as new PresenceManager(namespace, replicaId).
  • Peers that disappear without a leave (e.g., tab crash) are eventually detected by heartbeat timeout, though the BroadcastChannel API provides immediate notification on tab close.

⚑ Performance Model

Local Operation Targets (Engine Overhead Only)

Operation Rust Native (SQLite WAL) Browser WASM (OPFS)
Single record read (from materialized CRDT state) 1–5 Β΅s 0.5–2 ms
Single record write (CRDT mutation + WAL commit) 5–20 Β΅s 1–5 ms
CRDT merge (single Yrs update into document) 1–5 Β΅s 10–50 Β΅s
E2EE encrypt (ChaCha20-Poly1305) 5–15 Β΅s 50–200 Β΅s
E2EE decrypt 5–15 Β΅s 50–200 Β΅s
Full operation path (write β†’ encrypt β†’ oplog β†’ subscription fire) 10–40 Β΅s 2–10 ms
Cross-tab notification (shared memory / BroadcastChannel) 2–10 Β΅s 1–5 ms
Snapshot load (1MB Yrs document from mmap) < 1 ms 10–50 ms

All benchmarks run via Criterion.rs. A > 10% regression fails the CI build.

End-to-End Sync Targets (Engine + Typical Network)

Operation Perceived Latency
Write β†’ visible on same device < 1ms (local write)
Write β†’ coordinator ACK 50–200ms (fast network)
Write β†’ visible on another device 100–500ms (fast network)
Reconnect + sync 1000 pending operations 1–5 seconds
New replica bootstrap (1MB snapshot) 500–2000ms

Why Local-First Feels 0ms

The UI updates before the network is involved. The write is committed to local SQLite/OPFS synchronously. The subscription fires synchronously after the local CRDT merge. The upload to the coordinator happens asynchronously in a background worker thread. Users see their action reflected immediately β€” network latency is invisible.


πŸš€ SDK Reference β€” Quickstart

Installation

# Browser / React
npm install @vaultsync/web @vaultsync/react

# Node.js server
npm install @vaultsync/node

# Next.js
npm install @vaultsync/web @vaultsync/react @vaultsync/next

Rust

[dependencies]
vaultsync-core = "0.1"

Initialize VaultSync (Browser + React)

import { VaultSync } from "@vaultsync/web"
import { VaultSyncProvider } from "@vaultsync/react"
import { PostgresCoordinator } from "@vaultsync/web/coordinator"

// Create the client with CRDT schema
const vaultsync = new VaultSync({
  storage:   "opfs",              // OPFS (SQLite via WASM) for browsers
  namespace: "workspace:core",
  replicaId: getDeviceId(),       // stable, unique device identifier
  coordinator: new PostgresCoordinator({
    connectionString: process.env.COORDINATOR_URL
  }),
  auth: {
    getToken: () => clerk.session?.getToken() // your JWT provider
  },
  sync: {
    reconnect:   { attempts: Infinity, maxDelay: "30s" },
    uploadRetry: { attempts: 10, backoff: "exponential" }
  },
  multiTab: {
    enabled:           true,
    electionTimeout:   3000,
    heartbeatInterval: 1000
  }
})

// Define CRDT-typed schema
vaultsync.schema.define("todos", {
  fields: {
    id:        { type: "string",  crdtType: "lww",     primaryKey: true },
    text:      { type: "string",  crdtType: "lww",     indexed: true    },
    completed: { type: "boolean", crdtType: "lww"                       },
    priority:  { type: "number",  crdtType: "counter"                   },
    tags:      { type: "array",   crdtType: "orset",   indexed: true    },
    assignee:  { type: "string",  crdtType: "lww",     indexed: true    },
    localNote: { type: "string",  crdtType: "lww",     sync: false      } // never synced
  },
  softDelete: true  // DELETE adds tombstone, not hard delete β€” required for correct sync
})

// Register migrations
vaultsync.migration("v1", async (db) => {
  await db.defineDocument("todos", { /* initial schema */ })
})
vaultsync.migration("v2", async (db) => {
  await db.addField("todos", "priority", { type: "number", crdtType: "counter" })
})

// Initialize β€” runs migrations, sets up E2EE keys, connects to coordinator
await vaultsync.initialize()

// Wrap your app
export default function App() {
  return (
    <VaultSyncProvider client={vaultsync}>
      <MyApp />
    </VaultSyncProvider>
  )
}

CRUD Operations

const db = vaultsync.db

// INSERT β€” creates a new CRDT document (instant local write)
await db.todos.insert({
  id:        "todo:123",
  text:      "build vaultsync",
  completed: false,
  tags:      ["backend", "rust"],
  priority:  3
})

// READ β€” from local CRDT materialized state (no network, instant)
const todos    = await db.todos.findAll()
const todo     = await db.todos.findById("todo:123")
const filtered = await db.todos.findAll({
  where:   { completed: false, tags: { contains: "backend" } },
  orderBy: { field: "priority", direction: "desc" },
  limit:   50
})

// UPDATE β€” produces a Yrs binary CRDT diff, instantly visible in UI
await db.todos.update("todo:123", { text: "build vaultsync runtime" })

// DELETE β€” soft delete (tombstone) if softDelete: true in schema
await db.todos.delete("todo:123")

// BATCH β€” multiple CRDT mutations in one atomic transaction
await db.batch([
  db.todos.insert({ id: "todo:456", text: "write docs" }),
  db.todos.update("todo:123", { completed: true })
])

Key Management

// Keys are generated automatically on initialize()
// Manual management for advanced use cases:

await vaultsync.keys.rotate()                // Rotate namespace key
const pubKey   = await vaultsync.keys.publicKey()   // Export public key
const keyInfo  = await vaultsync.keys.status()
// { version: 2, createdAt: ..., algorithm: "X25519+ChaCha20-Poly1305" }

πŸ“ Schema System & Migrations

Schema Definition

vaultsync.schema.define("projects", {
  fields: {
    id:          { type: "string",  crdtType: "lww",     primaryKey: true },
    name:        { type: "string",  crdtType: "lww",     indexed: true    },
    description: { type: "string",  crdtType: "text"                      }, // Rich text CRDT
    memberCount: { type: "number",  crdtType: "counter"                   }, // PN-Counter
    tags:        { type: "array",   crdtType: "orset"                     }, // OR-Set
    ownerId:     { type: "string",  crdtType: "lww",     indexed: true    },
    draft:       { type: "boolean", crdtType: "lww",     sync: false      } // Local only
  },
  indexes: [
    { fields: ["ownerId"] },
    { fields: ["name", "memberCount"] }
  ],
  softDelete: true
})

CRDT Type as the Conflict Strategy

crdtType Merge Behavior Real-World Use
lww Last-write-wins by hybrid logical clock Title, status, any scalar
counter All increments/decrements sum globally Likes, votes, inventory
orset Add/remove with tombstone tracking Tags, members, feature flags
text Sequence CRDT (position-based insert/delete) Rich text bodies
custom Developer-defined Yrs extension Complex domain logic

Versioned Migrations

Migrations are additive only. Existing CRDT fields can never be removed (tombstones handle deletion). CRDT type of a field cannot be changed (use a new field name). This ensures all replicas on any schema version can accept mutations from other versions without data loss.

vaultsync.migration("v1", async (db) => {
  await db.defineDocument("todos", {
    fields: { id: { type: "string", crdtType: "lww", primaryKey: true },
              text: { type: "string", crdtType: "lww" } }
  })
})

vaultsync.migration("v2", async (db) => {
  await db.addField("todos", "priority", { type: "number", crdtType: "counter" })
})

vaultsync.migration("v3", async (db) => {
  await db.addField("todos", "tags", { type: "array", crdtType: "orset" })
})

Schema migration checksums are stored in vaultsync_migrations and verified on every apply. Tampered migrations are rejected.


πŸ”­ Observability & Debuggability

OpenTelemetry Traces β€” Every Operation

Trace: "vaultsync.write" (trace_id: abc123)
β”œβ”€β”€ Span: "crdt.merge"          β†’  2.1 Β΅s  β€” Yrs merge into document
β”œβ”€β”€ Span: "e2ee.encrypt"        β†’  7.3 Β΅s  β€” ChaCha20-Poly1305 encrypt
β”œβ”€β”€ Span: "oplog.append"        β†’  1.5 Β΅s  β€” Write to local oplog
β”œβ”€β”€ Span: "subscription.fire"   β†’  0.8 Β΅s  β€” Fire local subscription
└── Span: "transport.send"      β†’ 45 ms    β€” Upload to coordinator
    β”œβ”€β”€ Span: "coord.validate"  β†’  2 ms
    β”œβ”€β”€ Span: "coord.store"     β†’  5 ms
    └── Span: "coord.fanout"    β†’ 20 ms    β€” Push to other replicas

Exportable to Jaeger, Datadog, Grafana Tempo, or any OTLP-compatible backend.

Prometheus Metrics

Metric Description
vaultsync_mutations_total Total mutations processed (by status, namespace)
vaultsync_mutations_pending Current upload queue depth
vaultsync_sync_lag_ms Time from local write to coordinator ACK
vaultsync_download_lag_ms Time from coordinator sequence to replica apply
vaultsync_encryption_time_us E2EE encrypt/decrypt duration
vaultsync_crdt_merge_time_us CRDT merge duration per document
vaultsync_connection_status 1=connected, 0=disconnected
vaultsync_leader_status 1=leader, 0=reader
vaultsync_push_mutations_received Counter β€” mutations delivered via push (DownloadQueue)
vaultsync_snapshots_applied Counter β€” snapshot catch-ups applied (DownloadQueue)
vaultsync_optimistic_writes Counter β€” optimistic local writes (client.rs insert/update/delete)
vaultsync_hlc_logical_wraps Counter β€” HLC logical counter overflow events (HybridLogicalClock)
vaultsync_active_peers Gauge β€” active replicas in namespace (PresenceManager)

Debug HTTP API

GET  /debug/vaultsync/state           β†’ Full internal state as JSON
GET  /debug/vaultsync/state/oplog     β†’ Last 1000 oplog entries
GET  /debug/vaultsync/state/documents β†’ CRDT document snapshot metadata
GET  /debug/vaultsync/leader          β†’ Current leader status
GET  /debug/vaultsync/metrics         β†’ Prometheus endpoint
POST /debug/vaultsync/force-sync      β†’ Trigger immediate sync
POST /debug/vaultsync/force-election  β†’ Trigger leader re-election

CLI Tool

# Attach to a running VaultSync process
vaultsync inspect --pid 1234

# Watch live operation stream
vaultsync inspect --pid 1234 --stream

# Dump current CRDT document state
vaultsync inspect --pid 1234 --state

# Export and replay a trace file for deterministic debugging
vaultsync inspect --pid 1234 --export-trace > trace.json
vaultsync replay trace.json

πŸ›‘οΈ Security Model

Layers of Security

Layer Mechanism
Transport All sync traffic over TLS (WSS). Even if TLS is compromised, coordinator cannot read data (E2EE).
Application data E2EE via X25519 + ChaCha20-Poly1305. Coordinator stores only encrypted blobs.
Authentication Every coordinator connection requires a JWT from your auth provider (Clerk, Auth0, etc.). Expired tokens close the connection.
Namespace isolation A replica authorized for workspace:alpha cannot receive or push mutations for workspace:beta. Enforced at the coordinator.
Local storage OS-level sandboxing (OPFS is origin-private). Optional SQLCipher full-database encryption. Private keys stored in OS keychain (macOS Keychain, Windows Credential Manager).
Key pinning TOFU (Trust-On-First-Use) by default. Strict mode available: any new replica key requires manual operator approval before sync proceeds.

For Regulated Industries

const vaultsync = new VaultSync({
  namespace: "healthcare:patient-records",
  e2ee: {
    keyValidation: "strict",           // Manual approval for new replica keys
    onNewKeyDetected: async (replicaId, newKey) => {
      await notifySecurityTeam(replicaId, newKey)
      return "reject" // "accept" | "reject"
    }
  },
  storageEncryption: {
    enabled: true,
    mode: "sqlcipher",                 // Full database encryption
    keyDerivation: "os-keychain"       // AES-256-CBC, key in OS keychain
  }
})

🌍 Real-World Use Cases

Collaborative Todo / Project Management

Alice (laptop) adds todo:123 β€” instant UI update, CRDT mutation queued
      ↓ (async, encrypted)
Coordinator sequences and distributes encrypted mutation
      ↓ (50–200ms on good connection)
Bob (phone) receives encrypted mutation, decrypts, CRDT merges
Bob sees todo:123 appear without refreshing

Both users edit todo:123 simultaneously:
Alice: UPDATE { text: "updated title" }
Bob:   UPDATE { completed: true }

CRDT merge: { text: "updated title", completed: true }
Both changes preserved β€” no data loss, no conflict dialog.

Offline-First Mobile App (Airplane Mode)

User opens app on airplane (no connectivity):
  β†’ All reads served from local CRDT state β€” instant, zero latency
  β†’ User makes 47 changes during the flight
  β†’ 47 encrypted CRDT mutations queued in oplog, status: pending
  β†’ Device lost? Oplog encrypted at rest β€” attacker sees only ciphertext

Plane lands, connectivity returns:
  β†’ VaultSync reconnects, uploads 47 mutations in batches
  β†’ Downloads 12 mutations from other devices
  β†’ CRDT merges all 12 β€” no data lost, no conflicts
  β†’ App fully synchronized β€” coordinator never saw plaintext

Multi-Tab Web App

User opens same app in 3 browser tabs:
  Tab A (Leader): handles all writes
  Tab B (Reader): reads via shared memory, < 5ms
  Tab C (Reader): reads via shared memory, < 5ms

User closes Tab A (leader crash):
  Tab B detects heartbeat timeout (> 3s)
  Tab B acquires lock β†’ becomes Leader
  Tab B replays in-flight mutations from shared memory
  Tab C detects new leader via BroadcastChannel
  Zero data loss. Zero interruption. Users notice nothing.

Regulated Healthcare / Legal / Finance

Patient data synced across clinic devices:
  β†’ E2EE: coordinator (even if breached) cannot read PHI
  β†’ HIPAA compliance: no plaintext in coordinator logs
  β†’ Audit trail: OpenTelemetry traces every CRDT mutation
  β†’ On-prem coordinator: all data stays in clinic's network
  β†’ Key rotation: staff departures trigger namespace key rotation

Developer Tools & AI Applications

AI assistant stores context locally as CRDT documents:
  β†’ Reads and writes instant β€” no latency on AI completions
  β†’ Context syncs across devices when online
  β†’ Works fully offline β€” context available on airplane
  β†’ No server reads required for inference β€” cost drops by 90%

P2P LAN Collaboration

Two developers on the same office WiFi:
  β†’ Coordinator: Postgres in cloud (persistence, third-party access)
  β†’ P2P transport: WebRTC enabled for same-LAN peers
  β†’ Low-latency edits: P2P path (< 10ms round-trip)
  β†’ Developer leaves office: P2P disconnects, coordinator takes over
  β†’ Transparent failover. No data loss.

βš”οΈ VaultSync vs The World

Feature VaultSync ElectricSQL Zero (Rocicorp) PowerSync REST + WebSocket
Offline first βœ… Full βœ… βœ… βœ… ❌
Conflict resolution βœ… CRDT (no loss) ⚠️ LWW ⚠️ LWW ⚠️ LWW ❌ Server wins
E2EE βœ… Default, zero-trust ❌ ❌ ❌ ❌
Multi-tab safe βœ… Leader election ❌ ❌ ❌ ❌
Coordinator choice βœ… Any via trait ❌ Postgres only ❌ Proprietary ❌ MongoDB N/A
Perceived 0ms writes βœ… Local-first βœ… βœ… βœ… ❌ Network required
Reduce API costs βœ… Reads never hit server βœ… βœ… βœ… ❌ All reads hit server
Open source βœ… Apache 2.0 βœ… βœ… ❌ N/A
Rust core + WASM βœ… ❌ ❌ ❌ N/A
Observability βœ… OpenTelemetry Partial Partial ❌ N/A

πŸš€ Deployment Modes

Mode 1: Local-Only (No Sync)

const vaultsync = new VaultSync({
  storage:   "sqlite",
  namespace: "local",
  sync:      false    // no coordinator β€” offline-only
})

Mode 2: Embedded + Managed Coordinator

const vaultsync = new VaultSync({
  storage:     "opfs",
  namespace:   `user:${userId}`,
  coordinator: new PostgresCoordinator({ connectionString: process.env.DATABASE_URL })
})

Mode 3: Self-Hosted Coordinator Binary

const vaultsync = new VaultSync({
  storage:     "sqlite",
  namespace:   "workspace:core",
  coordinator: new CustomCoordinator({ url: "wss://your-coordinator.example.com/vaultsync" })
})

Mode 4: Multi-Instance Server Sync

App Server A (VaultSync embedded, namespace: workspace:core)
App Server B (VaultSync embedded, namespace: workspace:core)
App Server C (VaultSync embedded, namespace: workspace:core)
              β”‚
     Shared Coordinator (Postgres)

Mode 5: Hybrid Client + Server + P2P

const vaultsync = new VaultSync({
  namespace:  "workspace:core",
  coordinator: new PostgresCoordinator({ ... }),
  transports: {
    coordinator: true,
    p2p: { enabled: true },   // WebRTC for same-LAN peers
    mesh: { enabled: false }  // libp2p mesh (future)
  }
})

🀝 Contributing

We welcome contributions from the community! See CONTRIBUTING.md for:

  • Local development setup
  • Coding standards (Clippy + rustfmt required, zero warnings policy)
  • Conventional Commits format
  • Branch naming conventions
  • How to add a new coordinator backend
  • How to add a new storage backend
  • Fuzzing guide
# Get started
git clone https://github.com/parv68/VaultSync.git
cd vaultsync
./scripts/setup-dev.sh

# Run all tests
./scripts/run-all-tests.sh

# Run Rust tests only
cargo test --workspace --exclude vaultsync-fuzz

# Run SDK tests
npm test -w @vaultsync/web
npm test -w @vaultsync/node
npm test -w @vaultsync/react
npm test -w @vaultsync/next

βœ… Production Checklist

Before deploying VaultSync to production:

  • E2EE keys generated β€” verify keys are backed up; export public keys for recovery
  • Coordinator deployed β€” Postgres (recommended), Redis, or self-hosted server
  • Replica JWT auth configured β€” auth.getToken returns valid tokens for all sessions
  • Namespace authorization β€” limit which replicas can push/pull per namespace
  • Soft delete enabled on all synced tables β€” prevents sync gaps on delete operations
  • Schema migrations tracked β€” every field addition has a versioned migration
  • CRDT types chosen per field β€” LWW / Counter / OR-Set / Text selected intentionally
  • Oplog compaction configured β€” snapshot interval + tombstone GC threshold set
  • Multi-tab election tested β€” verified 3+ tab crash recovery scenario
  • Coordinator backups running β€” snapshot + mutation log backup to durable storage
  • OpenTelemetry configured β€” traces, metrics, logs exported to your observability backend
  • Sync lag alerting set up β€” alert if vaultsync_sync_lag_ms > threshold
  • Offline replica alerts β€” alert if a replica hasn't synced within tombstone retention
  • Graceful shutdown β€” server replicas flush pending uploads before process exit
  • Chaos tests passed β€” network partition, leader crash, clock skew, key rotation mid-sync
  • Benchmark regression gates green β€” no > 10% performance regressions vs main branch

πŸ“„ License

VaultSync is licensed under the Apache License, Version 2.0. See LICENSE for the full text.


πŸ“– Further Reading


VaultSync β€” Conflict-free, encrypted, infrastructure-agnostic synchronization for the local-first era.

About

Embedded local-first sync runtime for building offline-capable applications with automatic conflict resolution and eventual consistency.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages