Offline-first bidirectional database sync. Sequence numbers for speed, Merkle trees for correctness.
Clients keep a local SQLite database that syncs with a Postgres-backed server over Phoenix channels. Writes happen locally and sync when connected — no loading spinners, no network dependency for reads.
Client (SQLite) Server (Postgres)
┌─────────────┐ WebSocket / Phoenix ┌──────────────────┐
│ local writes│── push ─────────────► │ apply + broadcast│
│ │◄─ pull ────────────── │ seqnum triggers │
│ merkle tree │◄─ verify ──────────► │ row hashes │
└─────────────┘ └──────────────────┘
Layer 1 — Sequence numbers: Every write gets a monotonically increasing seqnum. Clients track the last-seen seqnum per table and pull only newer rows. Fast incremental sync.
Layer 2 — Merkle trees: Periodic SHA256-based integrity checks compare hash roots between client and server, drill into differing blocks, and repair only the rows that drifted.
Synclib supports two Merkle approaches depending on your needs:
-
Server-authoritative hashes (default): The server computes
row_hashvia Postgres triggers (pg_synclib_hash) and clients store the server-provided value. Merkle comparison checks that client and server have the same data — if hashes differ, the data drifted and gets repaired. This is the simpler approach and is preferred for most sync use cases where the goal is consistency between devices. -
Client-verified hashes: Clients use
synclib_hashdirectly to compute hashes locally from their own data, then compare against the server's hashes. This verifies correctness — that the data wasn't corrupted in transit or storage. Use this when your application needs to guarantee data integrity beyond just sameness (e.g., financial records, medical data, audit trails).
Both approaches use the same synclib_hash library and produce identical hashes, so they can be mixed. Most applications should start with server-authoritative hashes and add client-side verification only where correctness guarantees are needed.
| Repo | Description |
|---|---|
| synclib-server | Elixir/Phoenix sync server with seqnum triggers, Merkle verification, and scoped broadcasting |
| Repo | Description |
|---|---|
| synclib-client-js | TypeScript sync client — channels, push/pull, Merkle verification |
| synclib_client_flutter | Dart/Flutter sync client — same capabilities, native mobile + desktop |
| Repo | Description |
|---|---|
| synclib-js | TypeScript/JavaScript SQLite wrapper with automatic change tracking |
| synclib_flutter | Dart/Flutter SQLite wrapper with automatic change tracking |
| Repo | Description |
|---|---|
| synclib-hash | Cross-platform Merkle tree hashing (WASM + native). Used by pg-synclib-hash for server-side computation and available to clients for local correctness verification |
| pg-synclib-hash | PostgreSQL extension — computes row hashes at write time via triggers (server-authoritative) |
| Repo | Description |
|---|---|
| synclibc | C library for SQLite change tracking — compiled to native and WASM, used by all client wrappers |
| Repo | Description |
|---|---|
| synclib-test-harness | End-to-end test harness for validating sync across clients and server |
| Repo | Description |
|---|---|
| synclib-firestore-flutter | Dart/Flutter Firestore-compatible API for local SQLite via synclib |
| synclib-firestore-js | TypeScript Firestore-compatible API for local SQLite via synclib |
import { SyncClient, ChannelRole } from 'synclib-sync';
const client = new SyncClient({
db,
serverUrl: 'wss://api.example.com/socket',
clientId: 'device-abc',
channels: [{
topic: 'sync:user:user-123',
role: ChannelRole.PUSH,
tables: [{ name: 'todos' }],
}],
});
await client.initialize();
await client.connect(token);import 'package:synclib_sync/synclib_sync.dart';
final client = SyncClient(SyncClientConfig(
database: db,
serverUrl: 'wss://api.example.com/socket',
clientId: 'device-abc',
channels: [
SyncChannel(
topic: 'sync:user:user-123',
role: ChannelRole.push,
tables: [SyncTable('todos')],
),
],
));
await client.initialize();
await client.connect(token: token);mix deps.get && mix ecto.setup && mix phx.serverImplement the 6 behaviours for your use case — channel joins, snapshot queries, change handling, broadcast routing, connection hooks, and schema management.
- Offline-first — local SQLite with automatic change tracking. Works without a connection, syncs when online.
- Bidirectional — push, pull, or bidirectional per channel. Per-table direction overrides.
- Schema migrations — server-driven. Clients receive SQLite DDL over the sync channel and apply automatically.
- Multi-platform — TypeScript for web/Node.js, Dart/Flutter for iOS/Android/desktop, Elixir for the server.
- Phoenix channels — persistent WebSocket connections with scoped broadcasting (user, group, world).
- JWT auth — HS256 and RS256 token verification on every connection.
- Extensible — custom message types, conflict resolvers, and server-side behaviours.
synclibc (C)
├── synclib_flutter (Dart FFI)
│ └── synclib_client_flutter (sync client)
│ └── synclib-firestore-flutter (Firestore API)
├── synclib-js (TypeScript, WASM)
│ └── synclib-client-js (sync client)
│ └── synclib-firestore-js (Firestore API)
└── synclib-hash (WASM + native)
└── pg-synclib-hash (Postgres extension)
synclib-server (Elixir/Phoenix)
├── seqnum triggers
├── merkle verification
├── scoped broadcasting
└── schema management
All repositories are MIT licensed.