Sync
SwirlDB provides pluggable sync through the SyncAdapter trait. Built on CRDT technology,
sync is conflict-free and works offline-first. Multiple clients can edit simultaneously without
coordination - changes merge automatically.
Sync Architecture
CRDT-Based Sync
Powered by Automerge, SwirlDB sync is:
- Conflict-free: All changes merge automatically without manual resolution
- Offline-first: Work offline, sync when reconnected
- Incremental: Only transmit changes since last sync (delta sync)
- Namespace-based: Each room/document/tenant syncs independently
- Multi-client: Broadcast changes to all connected clients
βββββββββββ βββββββββββ
β Client β βββ Push Changes βββββ β Server β
β Alice β βββ Broadcast ββββββββ β CRDT β
βββββββββββ β Engine β
βββββββββββ
βββββββββββ β
β Client β βββ Push Changes ββββββββββββββ
β Bob β βββ Broadcast βββββββββββββββββ
βββββββββββ
All changes merge conflict-free Built-in Adapters
WebSocket Sync (Real-Time)
Bidirectional real-time sync over WebSockets. Changes pushed immediately to all clients.
// TypeScript/Browser
import { SwirlDB, WebSocketSync } from '@swirldb/js';
const db = await SwirlDB.withIndexedDB('my-app');
const sync = new WebSocketSync('wss://api.example.com/ws');
// Connect to namespace (room/document)
await sync.connect('room-123', db);
// All changes now auto-sync
db.data.messages.push({ from: 'Alice', text: 'Hello!' });
// Changes broadcast to all clients in room-123
// Disconnect when done
await sync.disconnect(); // Rust/Server
use swirldb_server::sync::WebSocketSync;
let sync = WebSocketSync::new("0.0.0.0:3030").await?;
// Server handles:
// - Client connections
// - Change merging
// - Broadcast to all clients
// - Persistent storage via redb
sync.run().await?; Features:
- Immediate propagation (< 50ms latency)
- Automatic reconnection with exponential backoff
- Binary protocol for minimal bandwidth
- Heartbeat/ping for connection health
Use case: Real-time collaboration, chat, live dashboards, multiplayer games
HTTP Sync (Long-Polling Fallback)
HTTP-based sync with long-polling for environments where WebSockets aren't available. Graceful fallback with similar API.
// TypeScript/Browser
import { SwirlDB, HttpSync } from '@swirldb/js';
const db = await SwirlDB.withLocalStorage('my-app');
const sync = new HttpSync('https://api.example.com/sync');
// Connect to namespace
await sync.connect('room-123', db);
// Push changes
db.data.user.name = 'Alice';
await sync.push();
// Long-poll for new changes (25s timeout)
const changes = await sync.poll();
db.applyChanges(changes);
// Automatic polling loop
sync.startPolling(5000); // Poll every 5s
sync.stopPolling(); HTTP Endpoints:
POST /sync/connect- Initial connection, returns full statePOST /sync/push- Push local changes to serverGET /sync/poll- Long-poll for new changes (25s timeout)
Use case: Corporate firewalls, restrictive networks, serverless deployments
WebRTC Sync (Peer-to-Peer) - Planned
Peer-to-peer sync using WebRTC data channels. Clients discover peers via signaling server.
// TypeScript/Browser (future)
import { SwirlDB, WebRTCSync } from '@swirldb/js';
const db = await SwirlDB.withIndexedDB('my-app');
const sync = new WebRTCSync({
signaling: 'wss://signal.example.com',
ice: [{ urls: 'stun:stun.l.google.com:19302' }]
});
// Join P2P room
await sync.join('room-123', db);
// Clients sync directly (no server storage)
db.data.doc.title = 'Collaborative Document';
// Server never sees plaintext data Status: π Planned
Use case: Privacy-first apps, local-first collaboration, serverless architectures
Sync Protocol
Binary WebSocket Protocol
Efficient binary protocol for minimal bandwidth usage:
Message Types:
ββββββββββββββββ¬βββββββ¬ββββββββββββββββββββββββββββββββββ
β Type β Code β Description β
ββββββββββββββββΌβββββββΌββββββββββββββββββββββββββββββββββ€
β MSG_CONNECT β 0x01 β Client joins namespace β
β MSG_SYNC β 0x02 β Server sends CRDT changes β
β MSG_PUSH β 0x03 β Client pushes local changes β
β MSG_BROADCASTβ 0x04 β Server broadcasts to all clientsβ
ββββββββββββββββ΄βββββββ΄ββββββββββββββββββββββββββββββββββ
Frame Format:
[1 byte: type][4 bytes: length][N bytes: payload]
Payload (CRDT changes):
- Binary Automerge changes
- Compressed with LZ4 (future)
- Encrypted end-to-end (optional) Incremental Sync
Only transmit changes since last sync using CRDT heads:
// Client sends current heads with MSG_CONNECT
const heads = db.getHeads();
// heads = [hash1, hash2, ...] - vector clock of last seen changes
// Server computes diff
let changes = namespace.getChangesSince(heads);
// Server sends only new changes
socket.send(MSG_SYNC, changes);
// Client applies incrementally
db.applyChanges(changes);
// Result: Minimal bandwidth, fast sync Conflict Resolution
CRDTs merge changes automatically using Automerge's built-in rules:
Conflict Type | Resolution Strategy
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββ
Text edits | Operational transform (OT)
Concurrent sets | Last-write-wins (LWW)
Counter increments | Commutative addition
List inserts | Position-based merge
Map updates | Per-key LWW
Deletes | Tombstone tracking
Example:
Alice: db.data.count = 5
Bob: db.data.count = 10
After sync:
- LWW: Highest timestamp wins
- Both clients converge to same value
- No "conflict markers" or manual resolution Sync Strategies
Real-Time Auto-Sync
Push every change immediately.
const db = await SwirlDB.withIndexedDB('my-app');
const sync = new WebSocketSync('wss://api.example.com/ws');
await sync.connect('room-123', db);
// Enable auto-sync (default)
sync.setAutoSync(true);
// Every mutation triggers sync
db.data.messages.push(newMessage); // Synced immediately
db.data.user.status = 'online'; // Synced immediately
// Batching with debounce (reduce network traffic)
sync.setDebounce(200); // Wait 200ms before syncing batch Periodic Sync
Sync on a fixed interval. Reduces network traffic, tolerates brief disconnections.
const sync = new HttpSync('https://api.example.com/sync');
await sync.connect('room-123', db);
// Sync every 10 seconds
setInterval(async () => {
await sync.push(); // Push local changes
const changes = await sync.poll(); // Get remote changes
db.applyChanges(changes);
}, 10000);
// Or use built-in polling
sync.startPolling(10000); Manual Sync
Explicit sync on user action.
const sync = new HttpSync('https://api.example.com/sync');
// Work offline
db.data.drafts.post1 = { title: 'New Post', content: '...' };
db.data.drafts.post2 = { title: 'Another Post', content: '...' };
// User clicks "Sync Now"
document.querySelector('#sync-btn').addEventListener('click', async () => {
try {
await sync.push();
const changes = await sync.poll();
db.applyChanges(changes);
showNotification('β
Synced successfully');
} catch (e) {
showNotification('β Sync failed - will retry');
}
}); Server Deployment
Production Server Setup
// Rust - native/swirldb-server/src/main.rs
use swirldb_server::{SyncServer, storage::RedbAdapter};
#[tokio::main]
async fn main() -> Result<()> {
// Configure logging
tracing_subscriber::fmt::init();
// Create persistent storage
let storage = RedbAdapter::new("./data/swirldb.redb")?;
// Start sync server
let server = SyncServer::builder()
.storage(storage)
.bind("0.0.0.0:3030")
.max_connections(10000)
.namespace_timeout(3600) // Remove idle namespaces after 1hr
.build()?;
tracing::info!("SwirlDB sync server listening on :3030");
server.run().await?;
Ok(())
} Docker Deployment
# Dockerfile
FROM rust:1.70 as builder
WORKDIR /app
COPY . .
RUN cargo build --release --bin swirldb-server
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/swirldb-server /usr/local/bin/
EXPOSE 3030
VOLUME /data
CMD ["swirldb-server"]
# docker-compose.yml
version: '3.8'
services:
swirldb:
image: swirldb-server:latest
ports:
- "3030:3030"
volumes:
- ./data:/data
environment:
- RUST_LOG=info
- SERVER_ID=prod-01 Load Balancing
For high-scale deployments, use sticky sessions (session affinity):
# nginx.conf
upstream swirldb {
ip_hash; # Sticky sessions based on client IP
server sync1.example.com:3030;
server sync2.example.com:3030;
server sync3.example.com:3030;
}
server {
location /ws {
proxy_pass http://swirldb;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
} Alternatively, use shared storage (Redis, Postgres) so any server can handle any client.
Security & Authentication
Namespace Access Control
Validate namespace access using JWT claims:
// Server-side authorization
async fn handle_connect(
socket: WebSocket,
namespace_id: String,
token: String,
) -> Result<()> {
// Validate JWT
let claims = validate_jwt(&token)?;
let actor = Actor::from_jwt_claims(claims)?;
// Check namespace access
if !can_access_namespace(&actor, &namespace_id) {
return Err("Unauthorized".into());
}
// Allow connection
let namespace = get_or_create_namespace(&namespace_id).await;
namespace.add_client(socket, actor).await;
Ok(())
}
fn can_access_namespace(actor: &Actor, namespace_id: &str) -> bool {
// Example: namespace format "org-{org_id}-{room_id}"
if let Some(org_id) = &actor.org_id {
namespace_id.starts_with(&format!("org-{}", org_id))
} else {
false
}
} End-to-End Encryption
Encrypt CRDT changes before transmission (server never sees plaintext):
import { AesGcmProvider } from '@swirldb/js';
// Shared key (use key exchange in production)
const encryption = await AesGcmProvider.fromPassword('room-password', 'room-123');
// Encrypt changes before sync
const changes = db.getChanges();
const encrypted = await Promise.all(
changes.map(c => encryption.encrypt(c))
);
// Send encrypted changes
await sync.push(encrypted);
// Server stores encrypted blobs (no access to content)
// Other clients decrypt
const remoteChanges = await sync.poll();
const decrypted = await Promise.all(
remoteChanges.map(c => encryption.decrypt(c))
);
db.applyChanges(decrypted); Performance Optimization
Bandwidth Reduction
- Delta sync: Only transmit changes since last sync (automatic)
- Compression: Use LZ4 or gzip for CRDT changes (future)
- Batching: Debounce rapid changes into single sync operation
- Selective sync: Only sync paths marked with
Syncedhint
// Reduce bandwidth by 90% with batching
sync.setDebounce(1000); // Batch changes for 1 second
// 100 rapid edits...
for (let i = 0; i < 100; i++) {
db.data.counter = i;
}
// Result: Single sync with final state, not 100 syncs Connection Management
- Heartbeat: Ping every 30s to detect dead connections
- Reconnect: Exponential backoff (1s, 2s, 4s, 8s, max 60s)
- Resume sync: Use heads to resume from last successful sync
- Offline queue: Queue changes while offline, sync when reconnected
Scalability
- Namespace sharding: Different namespaces on different servers
- Idle cleanup: Remove namespaces from memory after inactivity
- Lazy loading: Load namespaces on-demand from persistent storage
- Metrics: Track active connections, namespace count, message throughput
Future Enhancements
- WebRTC peer-to-peer sync (no server required)
- Compression (LZ4, Brotli) for CRDT changes
- Partial sync (sync only changed documents in large namespace)
- Sync priorities (high-priority changes first)
- Conflict callbacks (custom resolution logic)
- Time-travel debugging (replay sync history)
- Cross-region replication with eventual consistency
- GraphQL subscriptions adapter