From 9cf7c0f0073619433fb737590875270550d8dbde Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 19 Jul 2026 09:43:04 -0400 Subject: [PATCH] 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 --- docker/site-a-node-a/appsettings.Site.json | 19 ++++++++++++++++++- docker/site-a-node-b/appsettings.Site.json | 12 +++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docker/site-a-node-a/appsettings.Site.json b/docker/site-a-node-a/appsettings.Site.json index 7c841b41..53be3432 100644 --- a/docker/site-a-node-a/appsettings.Site.json +++ b/docker/site-a-node-a/appsettings.Site.json @@ -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" + } } } diff --git a/docker/site-a-node-b/appsettings.Site.json b/docker/site-a-node-b/appsettings.Site.json index 299055cc..8e7bd55c 100644 --- a/docker/site-a-node-b/appsettings.Site.json +++ b/docker/site-a-node-b/appsettings.Site.json @@ -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" + } } }