cbc-bridge-service — Sync Contract
Version: 1.0 Status: Draft Owner: Government / Emergency Last Updated: 2026-04-21
Companion: API_CONTRACTS · EVENT_SCHEMAS · DATA_MODEL · DEPLOYMENT_TOPOLOGY Related ADR: ADR-0004 §5 multi-region topology
This document defines what other services depend on from cbc-bridge-service, what it depends on from others, and the per-aggregate conflict & replication policies.
1. Consumers of cbc-bridge-service
| Caller | Interface | Dependency type | SLA expectation |
|---|---|---|---|
| Government clients (civil defence, NDMA, police) | gRPC BroadcastEmergency / CancelBroadcast / GetBroadcastStatus over mTLS + national-PKI signature | Synchronous (low-volume, high-severity) | P95 ≤ 1 500 ms; availability 99.99% during declared emergencies |
regulator-portal-service | gRPC BroadcastEmergency (on behalf of regulator) | Synchronous | Same |
admin-dashboard | HTTPS REST /v1/cbc/* | Synchronous admin surface | P95 ≤ 500 ms; availability 99.5% |
notification-service | NATS consumer (cbc.broadcast.*, cbc.drill.*) | Async fan-out | Eventually consistent within 5 s |
analytics-service | NATS consumer (cbc.audit.v1, others) | Async analytics | Eventually consistent within 60 s |
| Media partners (CBC-US-017) | HMAC-signed webhook from notification-service (public drill feed) | Async | Best-effort |
Sync contract semantics
The gRPC call is synchronous from the caller's perspective. Return implies:
- Signature was HSM-verified (persisted to
cbc.signature_audit) - Caller authorised by registry
Broadcastrow persisted with hash chain computedcbc.broadcast.requested.v1scheduled for outbox publish within 1 s
Dispatch (CBE send + ack) is asynchronous. The caller polls GetBroadcastStatus or subscribes to cbc.broadcast.*.v1 for the final verdict.
Fail-closed for authentication. Fail-open per-MNO for dispatch. Any HSM/PKI failure rejects the submission. Once accepted, dispatch fans out to all MNOs; one MNO's failure yields PARTIAL rather than blocking others (DOMAIN §6).
2. Dependencies of cbc-bridge-service
| Dependency | Interface | Failure mode if unavailable |
|---|---|---|
PostgreSQL cbc schema | SQL via pgbouncer (transaction pooling) | Handler returns CBC_DEPENDENCY_UNAVAILABLE 503; caller retries; no audit gap (fail-closed) |
| Redis (DB 5) | SET/GET/ZADD/SETNX | Idempotency + replay detection fall back to DB (UC-01 nonce_audit table); latency up; if DB also unavailable, fail-closed |
| NATS JetStream | Publish via outbox relay | Publish retried in relay; state is authoritative in Postgres |
| HSM (PKCS#11) | C_Verify per broadcast | Fail-closed. All BroadcastEmergency rejected with CBC_HSM_UNAVAILABLE. Manual fallback per FAILURE_MODES FM-01 |
| Vault (AppRole) | Per-MNO CBE credentials, HSM slot unsealing | Pods cannot boot without Vault bootstrap; running pods cache creds 15 min |
| MNO CBE endpoints | Per-adapter TCP + vendor protocol over IPSec/leased link | Per-MNO FAILED/TIMEOUT; fail-open per-MNO → PARTIAL or FAILED |
| OCSP responder | HTTPS | CRL cache (4 h) serves; cache miss + OCSP unreachable → fail-closed reject |
| CRL publication URL | HTTPS (per national-PKI CA) | Redis cache serves; refresh retries |
regulator-portal-service producing regulator.ca.trust.updated.v1 | NATS | Current trust chain continues; alert if > 24 h stale |
3. Proto definition
syntax = "proto3";
package ghasi.sms.cbc.v1;
option go_package = "github.com/ghasi/sms-gateway/cbc/v1";
service CbcBridgeService {
rpc BroadcastEmergency(BroadcastEmergencyRequest)
returns (BroadcastEmergencyResponse);
rpc GetBroadcastStatus(GetBroadcastStatusRequest) returns (BroadcastStatus);
rpc CancelBroadcast(CancelBroadcastRequest) returns (CancelBroadcastResponse);
rpc ScheduleDrill(ScheduleDrillRequest) returns (ScheduleDrillResponse);
rpc VerifyAuthorisedCaller(VerifyAuthorisedCallerRequest)
returns (VerifyAuthorisedCallerResponse);
}
// See API_CONTRACTS.md §1 for full message shapes.
Proto package is ghasi.sms.cbc.v1. Breaking changes bump to v2 with a ≥ 90-day deprecation window in parallel.
4. Per-Aggregate Conflict Policy
The service writes authoritative state for each aggregate in a single region (region-pinned). Cross-region conflict is only possible under a split-brain scenario after manual failover; the policy per aggregate is:
| Aggregate | Policy | Rationale |
|---|---|---|
Broadcast | server_authoritative (region-pinned) | A broadcast originates in the region currently holding the active gRPC listener for its caller. Failover is human-gated per ADR-0004 §5 to prevent split-brain on broadcastId uniqueness. |
MnoDispatch | server_authoritative (region-pinned to same region as parent Broadcast) | Tied to the parent's region; never written in two regions simultaneously. |
AuthorisedCaller | server_authoritative with lww (last-write-wins) on UPDATE, append-only on authorised_callers_history | Registry is platform-wide; multi-master with logical replication per ADR-0004 §5. LWW on updated_at is safe because mutations are rare and dual-control is via dualControlPartners field. |
CellDatabase | Per-MNO snapshot_version monotonic; cross-region mirror | Only the primary region runs the weekly cron; mirror is read-only. Manual failover bumps the lineage. |
Drill | server_authoritative | Scheduler runs in primary region only; secondary region promotes only on failover. |
BroadcastAuditEntry | append_only; cross-region mirror | Hash-chain within a partition requires serial append; mirror is read-only. Under split-brain, each region's chain remains internally valid; reconciliation creates a new chainLineageId and appends both chains under a superposition row. |
SignatureAudit | append_only; cross-region mirror | Same as audit. |
outbox | append_only | Relay idempotency via event_id ensures duplicate publish is harmless. |
Any split-brain event is a CRITICAL incident (IR-CBC-{date}) and triggers a manual reconciliation runbook — the platform deliberately sacrifices automatic reconciliation to preserve audit defensibility.
5. Cross-Region Replication
Per ADR-0004 §5:
| Data class | Replication | Consistency |
|---|---|---|
cbc.broadcasts, cbc.mno_dispatches | Logical replication kbl → mzr (async, RPO ≤ 5 s) | Region-local primary |
cbc.audit | Logical replication kbl → mzr and JetStream mirror kbl → dxb (audit-only leaf) | Append-only; lag alerted |
cbc.signature_audit | Same as audit | |
cbc.authorised_callers | Logical replication multi-master kbl ↔ mzr | Conflict-free under LWH; rare mutations |
cbc.mno_cell_database | Snapshot ship kbl → mzr + S3 mirror | Weekly cron writes only in primary |
cbc.drills | Logical replication kbl → mzr | Scheduler only in primary |
cbc.outbox | Not replicated — region-local | Each region relays its own outbox to the co-located JetStream cluster |
cbc.nonce_audit | Not replicated — region-local | Replay detection is per-region (Redis + local table) |
Failover posture.
- Read failover is automatic for admin/regulator GET surfaces (Cloudflare + GeoDNS).
- Write failover (
BroadcastEmergency) is manual-gated — split-brain onbroadcastIduniqueness is unacceptable for audit. A declared failover reassigns the writer region and announces the new active listener to all government callers via a pre-shared mTLS SNI and DNS update. - The
dxbleaf is strictly an audit-mirror; no writes are accepted there.
6. Integration Point — Government Caller mTLS
Government clients present a national-PKI client certificate. The gRPC server:
- Accepts only certs chained to the configured national-PKI root (pinned by SHA-256 of issuer-cert DER).
- Validates the end-entity cert against
(certSubject, certFingerprintSha256)tuples incbc.authorised_callers. - Checks OCSP stapling on every handshake + CRL cache every 4 h (CBC-US-008).
- Rejects connections failing any of the above at TLS layer (TLS 1.3
bad_certificatealert); the application handler never runs.
# nginx/istio-style illustration (actual implementation is gRPC + SPIFFE for platform peers, national-PKI for gov callers)
server_ssl_client_verify on;
server_ssl_client_trusted_cert_file /etc/cbc-tls/nat-pki-root.pem;
server_ssl_client_verify_depth 3;
server_ssl_stapling on;
server_ssl_stapling_verify on;
server_ssl_verify_crl /etc/cbc-tls/crl-cache.pem;
7. MNO CBE Adapter Contract
Each adapter implements a narrow Go/TS interface; this is what we guarantee to every MNO integration:
interface CbeAdapter {
/** Send one or more CBS PDUs to the MNO CBE endpoint.
* Returns an opaque ack reference when the CBE accepts (not necessarily when it broadcasts).
* Must complete within `deadline` or throw `TIMEOUT`.
*/
send(params: {
mnoBindId: string;
cbsPdus: CbsPduPage[]; // per-language, per-page
cellIds: string[];
serialNumber: number;
messageIdentifier: number; // 4370..4379
deadline: Date; // default now() + 30s
traceId: string;
}): Promise<{ cbeAckReference: string; latencyMs: number }>;
/** Attempt best-effort cancellation of a previously accepted broadcast.
* Returns true if the CBE confirms cancel; false/throw if already broadcasting.
*/
cancel(params: { mnoBindId: string; cbeAckReference: string }): Promise<boolean>;
/** Consume async ack from CBE (webhook or callback connection).
* Emits `cbc.adapter.ack.internal.v1` for UC-04 aggregator.
*/
handleAckCallback(raw: unknown): Promise<void>;
/** Health probe — used by circuit breaker and readiness. */
health(): Promise<{ ok: boolean; latencyMs: number }>;
}
Implementations:
Standard3gppCbeAdapter— 3GPP TS 23.041 §10 procedure, wire-format per ETSI EN 302 117. Used for AWCC, Roshan.EricssonProprietaryCbeAdapter— Ericsson CBC "CBC-NBI" North-Bound Interface, XML over HTTPS. Used for MTN_AF.HuaweiProprietaryCbeAdapter— Huawei USPP-CBE protocol, JSON over HTTPS with HMAC-SHA256 per message. Used for Etisalat, Salaam.
New MNO = implement a new adapter + register in cbc.mno_bind_registry + populate cbc.cbe_credentials_ref row.
8. Outbox / Inbox patterns
Outbox (produced). Every DB transaction that mutates cbc.broadcasts, cbc.mno_dispatches, cbc.audit, cbc.signature_audit, cbc.drills, cbc.authorised_callers, or cbc.mno_cell_db_current inserts a corresponding row into cbc.outbox within the same transaction. The CbcOutboxRelay worker reads unpublished rows, publishes to JetStream with explicit ack, and marks published_at. Failures retry; duplicate publish is safe because consumers dedupe on eventId.
Inbox (consumed). regulator.ca.trust.updated.v1 arrives on a durable consumer. The handler:
- Reads new trust anchor payload (signed by regulator HSM).
- Verifies against the previous trust anchor's signature to prevent malicious rotation.
- Writes to Vault
secret/cbc/pki/trust-chain. - Signals the HSM trust-store reloader.
- Acks only after success.
9. Schema stability guarantees
gRPC
| Field | Stability |
|---|---|
BroadcastEmergencyRequest.* required fields | Stable |
Severity enum | Stable; additions non-breaking (clients MUST treat unknown as UNSPECIFIED) |
GeoTarget.kind | Stable |
| New optional fields | Non-breaking (proto3 semantics) |
REST
- Routes under
/v1/cbc/*maintain backwards compatibility within the major version. - Breaking changes require
/v2/cbc/*withSunsetheader on the legacy path.
Events
- Per EVENT_SCHEMAS §9.
10. Contract tests
- Pact tests verify
regulator-portal-service → cbc-bridge-service BroadcastEmergencycontract on every CI run. - Generated OpenAPI is diffed against previous snapshot; breaking changes require an ADR and the
/v2/prefix pattern. - gRPC reflection test ensures every
v1field survives code changes.