Rust
Planned. rusqlite + hyper or axum. Trigger-based capture.
hook-sync’s wire protocol is language-agnostic. If your language has SQLite and HTTP, you can implement hook-sync — and sync with existing Go, Bun, and Node nodes.
No consensus algorithm. No Raft. No coordinator. Just triggers + HTTP + ACK.
Five things, in any language:
_changes, _meta, _dead_letter tables_changes_changes, POSTs to peer, deletes on ACK/sync endpoint that receives changes, applies with timestamp conflict check, returns ACKsyncing flag to prevent infinite loop (trigger WHEN clause checks _meta)Read the full Wire Protocol spec — it’s ~300 lines. Here’s the summary:
{ "op": "INSERT", "table": "items", "row": { "id": "0191a2b3-...", "name": "foo", "value": 42, "updated_at": 1700000000000 }, "old_id": null}POST /syncContent-Type: application/jsonX-Node-Id: node1
{ "batch_id": 42, "changes": [ ... ]}{ "applied": 3, "ack": 42 }Last-write-wins by updated_at timestamp:
if existing.updated_at > incoming.updated_at: skip (keep existing — it's newer)else: INSERT OR REPLACE (apply incoming — it's newer)Both nodes converge to the same state. No divergence.
CREATE TABLE _changes ( id INTEGER PRIMARY KEY AUTOINCREMENT, op TEXT, table_name TEXT, row_id TEXT, row_data TEXT);
CREATE TABLE _meta (key TEXT PRIMARY KEY, value TEXT);INSERT OR IGNORE INTO _meta VALUES ('syncing', '0');
CREATE TRIGGER items_ai AFTER INSERT ON itemsWHEN (SELECT value FROM _meta WHERE key = 'syncing') = 0BEGIN INSERT INTO _changes(op, table_name, row_id, row_data) VALUES('INSERT', 'items', NEW.id, json_object('id', NEW.id, 'name', NEW.name, 'value', NEW.value, 'created_at', NEW.created_at, 'updated_at', NEW.updated_at));END;
CREATE TRIGGER items_au AFTER UPDATE ON itemsWHEN (SELECT value FROM _meta WHERE key = 'syncing') = 0BEGIN INSERT INTO _changes(op, table_name, row_id, row_data) VALUES('UPDATE', 'items', NEW.id, json_object('id', NEW.id, 'name', NEW.name, 'value', NEW.value, 'created_at', NEW.created_at, 'updated_at', NEW.updated_at));END;
CREATE TRIGGER items_ad AFTER DELETE ON itemsWHEN (SELECT value FROM _meta WHERE key = 'syncing') = 0BEGIN INSERT INTO _changes(op, table_name, row_id, row_data) VALUES('DELETE', 'items', OLD.id, json_object('id', OLD.id, 'name', OLD.name, 'value', OLD.value, 'created_at', OLD.created_at, 'updated_at', OLD.updated_at));END;every batchMs (default 50ms): rows = SELECT id, op, table_name, row_data FROM _changes ORDER BY id LIMIT 10000 if rows.empty: return
batch_id = rows.last.id changes = rows.map(parse_json)
response = POST peer_url + "/sync" headers: { "Content-Type": "application/json", "X-Node-Id": node_id } body: { batch_id, changes }
if response.ack == batch_id: DELETE FROM _changes WHERE id <= batch_id // else: retry next tick (changes stay in _changes)POST /sync handler: body = parse_json(request.body) UPDATE _meta SET value = '1' WHERE key = 'syncing'
for change in body.changes: if change.op == "DELETE": existing = SELECT updated_at FROM items WHERE id = change.old_id if existing && existing.updated_at > change.row.updated_at: continue // keep newer update, skip delete DELETE FROM items WHERE id = change.old_id
else: // INSERT or UPDATE existing = SELECT updated_at FROM items WHERE id = change.row.id if existing && existing.updated_at > change.row.updated_at: continue // keep newer, skip incoming INSERT OR REPLACE INTO items (columns...) VALUES (values...)
UPDATE _meta SET value = '0' WHERE key = 'syncing' commit transaction
return { applied: body.changes.length, ack: body.batch_id }Every synced table must have:
id — TEXT PRIMARY KEY (UUID, zero conflict across multi-writer nodes)updated_at — INTEGER (millisecond timestamp, for last-write-wins conflict resolution)UUIDv7 recommended — time-ordered IDs give sequential B-tree inserts.
| Language | File | Lines | Notes |
|---|---|---|---|
| Go | go/cmd/server/main.go |
~240 | Thin wrapper over trigger/ library |
| Bun | bun/server.ts |
~120 | Thin wrapper over js/ library |
| Node | node/server.js |
~110 | Thin wrapper over js/ library |
All three sync to each other — same wire protocol, same ACK-based reliability, same split-brain safety.
Rust
Planned. rusqlite + hyper or axum. Trigger-based capture.
Python
Planned. sqlite3 stdlib + aiohttp or FastAPI. Trigger-based capture.
PHP
Planned. PDO_SQLITE + native HTTP server. Trigger-based capture.
Wire Protocol
Full protocol spec — the source of truth for any implementation.