Dedicated Hub
import { Aside } from ‘@astrojs/starlight/components’;
Dedicated Hub (Star)
Section titled “Dedicated Hub (Star)”For 8+ nodes, full mesh generates too much traffic per node. Use a dedicated hub — a Go-only relay binary that forwards changes between edges.
edge1 ──POST /sync──→ hub ──POST /sync──→ edge2edge3 ──POST /sync──→ hub ──POST /sync──→ edge4When to Use
Section titled “When to Use”- 8+ nodes (full mesh scaling limit exceeded)
- Regional relay (edges in different datacenters, hub in central location)
- Mixed runtime cluster (Go hub + Bun/Node edges)
Why Dedicated Hub (Not Dual-Purpose)
Section titled “Why Dedicated Hub (Not Dual-Purpose)”A dual-purpose hub (serves /api/items AND relays) has a problem: the syncing flag suppresses triggers during apply, which also blocks forwarding. A dedicated hub has no triggers at all — all data enters via /sync, not local writes.
| Dual-purpose hub | Dedicated hub | |
|---|---|---|
Serves /api/items? |
Yes | No |
| Has triggers? | Yes | No |
syncing flag problem? |
Yes — blocks forward | No — no triggers |
| Client traffic? | Yes — competes with relay | No — pure relay |
1. Run the hub (Go binary)
Section titled “1. Run the hub (Go binary)”cd go && go build -o ../hook-sync-hub ./cmd/hub
./hook-sync-hub -id hub1 -listen :9010 -db hub1.pebble \ -edge http://localhost:9001 \ -edge http://localhost:9002 \ -edge http://localhost:9003The hub is a pure relay — no SQLite, no triggers, no /api/items. It stores a backup in Pebble KV and forwards changes to all edges.
2. Point edge nodes to the hub
Section titled “2. Point edge nodes to the hub”From any runtime, the hub is just a peer URL:
./hook-sync-mesh-go -id edge1 -db e1.db -listen :9001 \ -peer http://localhost:9010const mgr = attach(db, { id: "edge1", peers: ["http://localhost:9010"], // hub URL batchMs: 50,}, ["items"]);Edges don’t know it’s a hub — it’s transparent. No API changes.
What the Hub Does
Section titled “What the Hub Does”- Receive
/sync— accept changes from any edge - Apply to Pebble —
Set("data:{id}", rowJSON)for INSERT/UPDATE,Deletefor DELETE (backup copy) - Enqueue durable forwards —
Set("fwd:{n}", {batchID, changes, edgeURL})in Pebble before ACK - ACK edge immediately — edge deletes from its
_changes - Forward asynchronously — try immediate forward, background sweep retries with backoff
Durable Forwarding Queue (Pebble)
Section titled “Durable Forwarding Queue (Pebble)”Hub ACKs edge immediately on receive, then forwards to other edges asynchronously. If hub crashes after ACK but before forward, changes survive in Pebble’s durable forwarding queue → replay on restart.
edge1 → hub receives → Set("data:{id}", rowJSON) in Pebble ← backup → Set("fwd:{n}", {changes, edgeURL}) ← durable forward queue → ACK edge1 (edge deletes from its _changes) → forward to edge2/3/4 → each edge ACKs → Delete("fwd:{n}") from Pebble
hub crash after ACK, before forward? → Pebble still has fwd:{n} → restart → forwardSweep replays → forward → doneCrash Recovery
Section titled “Crash Recovery”Verified: kill hub mid-traffic → write 5 items while hub down → restart hub → all edges converge to equal count, 0 pending, 0 dead letter.
Hub Failure
Section titled “Hub Failure”Hub is a single point of failure. Mitigate: run hub ←→ hub-backup (point-to-point, already works). If hub dies, hub-backup takes over. Edges reconnect to hub-backup.
Benchmark
Section titled “Benchmark”1 Go hub + 3 edge nodes. 5 runs × 50 req per edge (150 total per run):
| Runtime (edges) | QPS median | QPS min | QPS max | Integrity |
|---|---|---|---|---|
| Go | 16,268 | 5,706 | 17,516 | 5/5 PASS |
| Bun | 19,750 | 12,931 | 27,683 | 5/5 PASS |
| Node | 13,193 | 148 | 21,660 | 5/5 PASS |
Integrity: all 3 edges have equal item count, hub backup count matches edges, 0 pending changes, 0 pending forwards, 0 dead letter.
Run with: bash bench-hub.sh