feat(localdb): enable replication on the site-a rig pair

Task 11. site-a-a is the initiator (dials scadabridge-site-a-b:8083, the
existing site gRPC h2c listener - no new port); site-a-b is passive. Both carry
the SAME dev ApiKey, which is load-bearing: LocalDbSyncAuthInterceptor is
fail-closed, so a mismatch does not degrade to unauthenticated replication, it
rejects every stream and the pair silently stops converging.

site-b and site-c stay unreplicated on purpose, so the default-OFF posture is
proven side-by-side on one rig rather than asserted.

Plain dev key matches the rig's existing posture; production uses a
${secret:...} reference through the pre-host expander. Noted in both files.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
This commit is contained in:
Joseph Doherty
2026-07-19 09:43:04 -04:00
parent 3c395d794a
commit 9cf7c0f007
2 changed files with 29 additions and 2 deletions
+18 -1
View File
@@ -64,6 +64,23 @@
// outside the volume and were lost on every recreate.
// Replication is opt-in and configured separately; absent = local-only.
"LocalDb": {
"Path": "/app/data/site-localdb.db"
"Path": "/app/data/site-localdb.db",
// Site-a is the rig's REPLICATED pair; site-b and site-c deliberately stay
// unreplicated so the default-OFF posture is proven side-by-side on one rig.
//
// This node is the initiator: it dials the peer. Replication is still
// bidirectional - the passive node's writes flow back over the same stream -
// so only one side needs PeerAddress. Port 8083 is the existing site gRPC
// listener (h2c); the sync endpoint shares it, no new port.
//
// The ApiKey must be IDENTICAL on both nodes: the host's
// LocalDbSyncAuthInterceptor is fail-closed, so a mismatch (or a missing key)
// rejects every sync stream. A plain dev key here matches the rig's existing
// posture; PRODUCTION uses a "${secret:...}" reference resolved by the
// pre-host secret expander.
"Replication": {
"PeerAddress": "http://scadabridge-site-a-b:8083",
"ApiKey": "dev-site-a-localdb-sync-key"
}
}
}
+11 -1
View File
@@ -64,6 +64,16 @@
// outside the volume and were lost on every recreate.
// Replication is opt-in and configured separately; absent = local-only.
"LocalDb": {
"Path": "/app/data/site-localdb.db"
"Path": "/app/data/site-localdb.db",
// The PASSIVE half of site-a's replicated pair: no PeerAddress, so this node's
// sync initiator starts and idles while node-a dials in. Its own writes still
// reach node-a - the stream is bidirectional.
//
// The key MUST match node-a's exactly. LocalDbSyncAuthInterceptor is
// fail-closed, so a typo here does not degrade to unauthenticated replication;
// it rejects every stream and the pair silently stops converging.
"Replication": {
"ApiKey": "dev-site-a-localdb-sync-key"
}
}
}