Point-to-Point
2 nodes, active-active. Simplest setup.
hook-sync works with Bun via the hooksync.js npm package and bun:sqlite. Trigger-based capture only (no preupdate_hook in bun:sqlite).
Install the library
bun add hooksync.jsCreate server.ts
import { attach } from "hooksync.js";import { Database } from "bun:sqlite";
const db = new Database("app.db");db.exec("PRAGMA journal_mode = WAL");db.exec(`CREATE TABLE IF NOT EXISTS items( id TEXT PRIMARY KEY, name TEXT, value INTEGER, created_at INTEGER, updated_at INTEGER);`);
const mgr = attach(db, { id: "node1", peers: ["http://localhost:9002"], batchMs: 50,}, ["items"]);
Bun.serve({ port: 9001, fetch(req) { const url = new URL(req.url); if (req.method === "POST" && url.pathname === "/sync") { return req.json().then((body) => { const applied = mgr.applyChanges(body.changes); return Response.json({ applied, ack: body.batch_id }); }); } if (req.method === "GET" && url.pathname === "/health") { return Response.json(mgr.health()); } return new Response("not found", { status: 404 }); },});Run two instances
bun server.ts # node1 on :9001# In another terminal, change port to 9002 and peer to 9001bun server.tsWrite to node A, verify on node B
curl -X POST http://localhost:9001/api/items \ -H 'Content-Type: application/json' \ -d '{"name":"hello","value":42}'
curl http://localhost:9002/api/items# [{"id":"...","name":"hello","value":42,...}]attach(db, config, tables) → Manager| Parameter | Type | Description |
|---|---|---|
db |
Database |
bun:sqlite instance. Caller opens it. |
config |
Config |
{ id: string, peers: string[], batchMs?: number, batchSize?: number } |
tables |
string[] |
Table names to sync. Triggers auto-generated via PRAGMA table_info. |
Returns a Manager:
| Method | Description |
|---|---|
applyChanges(changes) |
Apply received changes (LWW conflict resolution). Returns count applied. |
health() |
Returns { ok, node_id, item_count, pending_changes, dead_letter, peers, clock_skew_ms, clock_ok }. clock_skew_ms = max observed skew vs remote updated_at. clock_ok = false if skew ≥ 1000ms. |
stop() |
Stop the background ship loop. |
Bun.serve({ port: 9001, fetch(req) { const url = new URL(req.url);
// /sync — receive changes from peers if (req.method === "POST" && url.pathname === "/sync") { return req.json().then((body) => { const applied = mgr.applyChanges(body.changes); return Response.json({ applied, ack: body.batch_id }); }); }
// /health if (req.method === "GET" && url.pathname === "/health") { return Response.json(mgr.health()); }
// ... your CRUD endpoints here return new Response("not found", { status: 404 }); },});The pattern: parse JSON body, call mgr.applyChanges(body.changes), return { applied, ack: body.batch_id }.
Every synced table must have:
id — TEXT PRIMARY KEY (UUID, zero conflict)updated_at — INTEGER (millisecond timestamp, for last-write-wins)CREATE TABLE items( id TEXT PRIMARY KEY, name TEXT, value INTEGER, created_at INTEGER, updated_at INTEGER);When you call attach(), the library:
_meta, _changes, _dead_letter, _peer_state tablesconst mgr = attach(db, { id: "nodeA", peers: [ "http://localhost:9002", "http://localhost:9003", "http://localhost:9004", ], batchMs: 50,}, ["items"]);Each peer has its own watermark. Changes deleted from _changes only after all peers ACK. See Full Mesh.
For 8+ nodes, use a dedicated hub — a Go-only relay binary. From the JS library’s perspective, the hub is just a peer URL:
const mgr = attach(db, { id: "edge1", peers: ["http://localhost:9010"], // hub URL — same as any peer batchMs: 50,}, ["items"]);See Dedicated Hub for hub setup.
UUIDv7 gives time-ordered IDs → sequential B-tree inserts. Bun: optimized hex-table impl (benchmark: bun/bench-uuid.ts).
function uuidv7(): string { const ts = Date.now(); const buf = new Uint8Array(16); crypto.getRandomValues(buf); // ... see bun/bench-uuid.ts for full implementation return /* uuid string */;}Bun.serve() or any frameworkbun:sqlite has no preupdate hook API. Trigger-based onlyPoint-to-Point
2 nodes, active-active. Simplest setup.
Full Mesh
3-7 nodes, all-to-all sync.
Dedicated Hub
8+ nodes. Go-only relay with Pebble KV.
Multi-Region
Hub-to-hub cross-region sync.