cbc-bridge-service — Application Logic
Version: 1.0 Status: Draft Owner: Government / Emergency Last Updated: 2026-04-21
Companion: DOMAIN_MODEL · API_CONTRACTS · DATA_MODEL · EVENT_SCHEMAS · FAILURE_MODES
1. Use Cases
UC-01: BroadcastEmergency (gRPC hot path)
Trigger. Authorised government caller (civil-defence, NDMA, police) or regulator-portal-service calls CbcBridgeService/BroadcastEmergency over mTLS with a detached PKCS#7 signature in the X-Gov-Signature header.
Input. BroadcastEmergencyRequest:
requestId(UUIDv4 idempotency key)headline(≤ 60 chars)bodyVariants(1–4 entries:en,fa,ps,ar)geoTarget(cell_ids[]|polygonGeoJSON |regionISO-3166-2 |country)severity(P0_EXTREME|P1_MAJOR|P2_ADVISORY)expiryMinutes(≤ 120 for P0; ≤ 240 for P1; ≤ 720 for P2)isDrill(bool)nonce(16-byte random; anti-replay)requestedAt(RFC 3339 UTC)
Output. BroadcastEmergencyResponse:
broadcastIdacceptedAt(RFC 3339 UTC)expectedDispatchBy(≤acceptedAt + 60 s)signatureAuditId
SLA. P95 ≤ 1 500 ms (HSM signature verify + DB write + outbox publish). P99 ≤ 3 000 ms.
Steps:
- Transport auth. mTLS handshake presents a client certificate chained to the national-PKI trust anchor. On OCSP-stapled revocation or CRL-cached revocation, connection is rejected at TLS layer with
unknown_caalert — no application code runs. - Replay-attack check. Compute
presentedCertFingerprint = sha256(clientCert.der). Reject withUNAUTHENTICATEDif:|now - requestedAt| > 120 s→REASON_CLOCK_SKEW(3GPP TS 23.041 dispatch window assumption)SETNX cbc:nonce:{presentedCertFingerprint}:{nonce} 1 EX 86400returns 0 (replay observed) →REASON_REPLAY
- Canonicalisation. Build the RFC 8785 JCS canonical JSON of the request body (excluding signature), compute
sha256digest. - HSM signature verify. Call the HSM (PKCS#11
C_Verify) with the presented cert's public key, the digest, and the detached signature. Any HSM failure is fail-closed (never fall back to in-process verification). Persist result tocbc.signature_audit. - Caller-registry lookup. Resolve
callerby(certSubject, certFingerprintSha256)incbc.authorised_callers WHERE active = TRUE AND notBefore ≤ now ≤ notAfter. If not found →PERMISSION_DENIEDwithREASON_CALLER_NOT_REGISTERED. - Scope check.
severity ∈ caller.allowedSeveritiesANDgeoTarget ⊆ caller.allowedRegions; otherwisePERMISSION_DENIEDwithREASON_CALLER_SCOPE_VIOLATION. - Idempotency. Hash
requestId → broadcastIdin Redis (cbc:idem:{requestId}24 h). Repeat requests return the originalbroadcastIdwithout side-effects. - Payload validation. Run UC-06 translation pre-flight to confirm each language encodes within the 15-page CBS limit; reject with
INVALID_ARGUMENTon overflow. - Persist
Broadcastrow. Transaction writes:cbc.broadcastsrow withstate = ACCEPTED, hash-chain fields computed against prior row in monthly partitioncbc.auditrow withtransition = REQUESTEDcbc.outboxeventcbc.broadcast.requested.v1Commit → outbox relay publishes to NATS within 1 s.
- Return response.
broadcastId,acceptedAt,expectedDispatchBy = acceptedAt + 60 s,signatureAuditId. - Async handoff.
DispatcherWorkerpicks upACCEPTEDbroadcasts (PostgresLISTEN/NOTIFY+ poll fallback) and runs UC-02 PerMnoDispatch.
Error taxonomy:
| gRPC status | Reason code | Condition |
|---|---|---|
INVALID_ARGUMENT | REASON_MISSING_LANGUAGE | P0 without en + fa + ps |
INVALID_ARGUMENT | REASON_PAGE_LIMIT | Any language encodes to > 15 CBS pages |
INVALID_ARGUMENT | REASON_GEO_UNKNOWN_CELLS | cell_ids[] contains unknown cells |
INVALID_ARGUMENT | REASON_POLYGON_OVERSIZED | Polygon area > national area |
UNAUTHENTICATED | REASON_SIGNATURE_INVALID | HSM verify = SIGNATURE_INVALID |
UNAUTHENTICATED | REASON_CLOCK_SKEW | ` |
UNAUTHENTICATED | REASON_REPLAY | Nonce reused |
PERMISSION_DENIED | REASON_CALLER_NOT_REGISTERED | Cert subject not in registry |
PERMISSION_DENIED | REASON_CALLER_SCOPE_VIOLATION | Severity or region outside caller scope |
FAILED_PRECONDITION | REASON_HSM_UNAVAILABLE | HSM session cannot be opened |
RESOURCE_EXHAUSTED | REASON_RATE_LIMITED | Per-caller rate limit (default 10 reqs/min for P0_EXTREME) |
UC-02: PerMnoDispatch (worker)
Trigger. DispatcherWorker observes state = ACCEPTED on cbc.broadcasts. Concurrency limit: cbcDispatcherMaxConcurrent (default 8 — emergencies rare).
Steps:
- Advisory lock.
pg_try_advisory_xact_lock(broadcastIdHashed)— exactly-once worker pickup. - State transition.
ACCEPTED → DISPATCHING; appendcbc.auditrow withtransition = DISPATCHED. - Adapter dispatch fan-out. For each active MNO (from
cbc.mno_bind_registry), in parallel with per-MNO 30 s deadline:- Load adapter strategy:
STANDARD_3GPP(AWCC, Roshan),ERICSSON_PROPRIETARY(MTN_AF),HUAWEI_PROPRIETARY(Etisalat, Salaam) — configured per MNO. - UC-03 ResolveCellIds against per-MNO
cbc.mno_cell_database. - UC-06 TranslateToCbs produces per-language CBS PDUs with Message Identifier, Serial Number, Data Coding Scheme, Page Parameter per 3GPP TS 23.041 §9.4.1.2.
- Write
cbc.mno_dispatchesrow withstatus = PENDING. - Invoke adapter:
adapter.send(mnoBindId, cbsPdus, resolvedCellIds, timeout = 30s)over the MNO CBE endpoint (TCP over leased/IPSec per ADR-0004 §6). - On success →
status = DISPATCHED+cbeAckReference; await async ACK. - On timeout →
status = TIMEOUT; do not retry automatically (regulator must investigate). - On vendor error →
status = FAILEDwitherrorCode+errorDetail; open per-adapter circuit breaker after 3 consecutive vendor errors within 60 s.
- Load adapter strategy:
- UC-04 AggregateAcks runs continuously and produces terminal state.
Budget. Sub-60 s dispatch target from ACCEPTED. The wall-clock budget is dominated by the slowest MNO (30 s cap) plus 3–5 s overhead. Per-MNO fail-open posture ensures a slow MNO does not block the others.
UC-03: ResolveCellIds
Trigger. Subroutine of UC-02.
Steps by geoTarget.kind:
| Kind | Algorithm |
|---|---|
CELL_IDS | Validate each cell id is present in per-MNO mno_cell_database. Unknown ids → fail UC-01 step 8 with REASON_GEO_UNKNOWN_CELLS. |
POLYGON | For each cell in the MNO cell database, compute ST_Within(cell.geom, polygon) (PostGIS). Expand polygon by accuracyMeters on cells near edges (buffered match). |
REGION | Load region polygon from cbc.regions_geom (ISO-3166-2 province/district). Use POLYGON algorithm. |
COUNTRY | Return every active cell for the MNO (no filtering). |
The resolved cellIds[] is frozen into the MnoDispatch row — subsequent MNO cell-DB refreshes do not retroactively change audit evidence (DOMAIN §2.4 invariant).
UC-04: AggregateAcks
Trigger. Event consumer on cbc.adapter.ack.internal.v1 (emitted by adapter when CBE asynchronously acknowledges), and periodic sweeper running every 2 s over DISPATCHING broadcasts.
Steps:
- For broadcast
bc_..., querycbc.mno_dispatches WHERE broadcastId = :idfor the latestattemptper MNO. - Compute:
ackedCount = count(status = ACKED)failedCount = count(status ∈ {FAILED, TIMEOUT, REJECTED})pendingCount = count(status ∈ {PENDING, DISPATCHED})
- Terminal-state rules:
pendingCount > 0and elapsed sincedispatchedAt<aggregatorDeadlineSeconds(default 45 s) → do nothing; wait.ackedCount = totalMnos→state = ACKED; emitcbc.broadcast.acked.v1.ackedCount ≥ 1 AND ackedCount < totalMnos(after deadline) →state = PARTIAL; emitcbc.broadcast.partial.v1.ackedCount = 0 AND failedCount = totalMnos→state = FAILED; emitcbc.broadcast.failed.v1.
- Append
cbc.auditrow with appropriatetransition. - Update
finalisedAt = now().
SLA. Finalisation within 60 s of BroadcastEmergency acceptance for ≥ 95% of broadcasts (per NFR catalog).
UC-05: CancelBroadcast (dual-control)
Trigger. Caller A invokes CancelBroadcast(broadcastId) within the 60 s dispatch window.
Steps:
- Verify signature (same path as UC-01 steps 1–4).
- Resolve caller. If
caller ≠ broadcast.callerandcaller.callerId ∉ broadcast.caller.dualControlPartners→PERMISSION_DENIED. SETNX cbc:cancel:pending:{broadcastId} {callerIdA}EX 60. If this is the first approver, return202 AWAITING_APPROVALwithapprovalToken.- On the second call (different caller),
GETDEL cbc:cancel:pending:{broadcastId}and verify caller is distinct. - Transaction:
- For each
mno_dispatches WHERE status ∈ {PENDING, DISPATCHED}: invoke adaptercancel(cbeAckReference)best-effort; markstatus = CANCELLEDif adapter acks, else leave and recorderrorDetail = CANCEL_FAILED. broadcast.state = CANCELLED; appendcbc.auditrow; emitcbc.broadcast.cancelled.v1.
- For each
- Return per-MNO breakdown of effective vs ineffective cancellations.
Invariant. MNOs already in ACKED cannot be cancelled (the broadcast has reached handsets in that MNO's network).
UC-06: TranslateToCbs (pre-flight)
Trigger. Subroutine of UC-01 (validation) and UC-02 (dispatch).
Converts a LanguageVariant into 3GPP TS 23.041 CBS PDU pages.
function translateToCbsPdu(headline, body, lang, severity, isDrill, serialNumber):
msgId = severityToMessageIdentifier(severity, isDrill)
# P0=4370, P1=4371, P2=4372 (DOMAIN §2.1), drill=4373..4379
dcs = (lang == 'en') ? 0x00 /* GSM-7 default alphabet */
: 0x11 /* UCS-2 */
# 3GPP TS 23.038 §5
localisedPrefix = isDrill ? localisedDrillPrefix(lang) : ""
fullText = localisedPrefix + headline + '\n' + body
# Encode per DCS
bytes = (dcs == 0x00) ? encodeGsm7(fullText)
: encodeUcs2BigEndian(fullText)
# Page sizing per 3GPP TS 23.041 §9.4.1.2
bytesPerPage = (dcs == 0x00) ? 82 : 82 # 82 octets of user data per page
pages = chunk(bytes, bytesPerPage)
if len(pages) > 15:
raise INVALID_ARGUMENT(REASON_PAGE_LIMIT)
return [
CbsPduPage(
messageIdentifier = msgId, # 16-bit
serialNumber = serialNumber, # 16-bit
dataCodingScheme = dcs, # 8-bit
pageParameter = (pageNo << 4) | totalPages, # 8-bit
cbData = pages[pageNo-1]
)
for pageNo in 1..len(pages)
]
function localisedDrillPrefix(lang):
# Per CBC-US-015; translations maintained in customer-portal glossary (EP-CUST-09)
return {
'en': 'DRILL - NO ACTION REQUIRED\n',
'fa': 'تمرین - هیچ اقدامی لازم نیست\n',
'ps': 'تمرین - هیڅ اقدام اړین نه دی\n',
'ar': 'تمرين - لا يتطلب أي إجراء\n',
}[lang]
The resulting PDU list is wrapped in an adapter-specific envelope (standard-3GPP, Ericsson, or Huawei) before being sent to the CBE.
UC-07: VerifyAuditChain (background worker)
Trigger. Cron 0 3 * * * (03:00 Asia/Kabul) via AuditVerifierWorker. Distributed lock cbc:audit:lock:verifier (EX 3600).
Steps:
- Scan yesterday's partition of
cbc.broadcastsandcbc.auditin primary-key order. - For each row, recompute
rowHash = sha256(canonicalJson(row minus rowHash) ‖ prevHash). - Compare to stored
rowHash. - On mismatch → emit
cbc.audit.chain.broken.v1, page on-call (CbcAuditChainBrokenCRITICAL), open incident IR-CBC-{date}. - On success → emit
cbc.audit.chain.verified.v1withverifiedUpto = maxOccurredAt.
The verifier is idempotent; re-running only re-emits the verified event.
UC-08: ScheduleDrill
Trigger. Platform admin invokes POST /v1/cbc/drill or internal DrillSchedulerWorker cron.
Steps:
- Create
cbc.drillsrow withstate = SCHEDULED. - Emit
cbc.drill.scheduled.v1. - At
scheduledAt, the scheduler:- Generates a system-caller signed
BroadcastEmergencyRequestusing the internal NDMA-surrogate cert (held in HSM slotcbc/drill-initiator). - Calls UC-01 with
isDrill = true,severity = P2_ADVISORY(drills default advisory), CBS test-slot Message Identifier (4373..4379, round-robin). - Awaits finalisation.
- Generates a system-caller signed
- After finalisation,
DrillReportWorker:- Computes per-MNO ack latency, resolvedCellIds count, partial-failure breakdown.
- Renders after-action report (HTML + PDF) to object storage.
- Emails NDMA + Ghasi NOC + ATRA observer (per CBC-US-016).
- Emits
cbc.drill.completed.v1withreportRef.
UC-09: RefreshMnoCellDatabase
Trigger. CellDbRefreshWorker cron 0 2 * * 1 (02:00 Monday Asia/Kabul).
Steps:
- For each MNO with an export endpoint:
- Fetch
GET https://{mno-endpoint}/cell-export?since={lastSnapshotAt}over IPSec or leased link, authenticated with per-MNO API key (Vaultsecret/cbc/mno/{mno}/cell-export-key). - Validate schema (
cellId,lat,lng,accuracyMeters, optionaldecommissioned). - Insert into a staging table.
- Atomic swap — in a single transaction, bump
snapshotVersion, replace rows formnoId, emitcbc.mno.cell.refreshed.v1. - Coverage metric: percent of national-area polygons (100 synthetic test polygons) resolvable to ≥ 1 cell. Alert
CbcCellDatabaseStaleif below 85%.
- Fetch
- Manual upload path (admin REST) executes the same staging+swap path.
UC-10: VerifyAuthorisedCaller (admin diagnostic)
Trigger. Admin invokes POST /v1/cbc/callers/verify with a candidate certificate + sample signature.
Steps:
- Execute UC-01 steps 1–6 without persisting a broadcast. Returns a synthetic
VerifyResult. - Use case: Legal or NDMA onboarding a new caller, verifying their PKI chain before the
mouRefis counter-signed.
2. Performance & Budgets
| Operation | Target | Enforcement |
|---|---|---|
BroadcastEmergency P95 | 1 500 ms | HSM verify ≤ 300 ms; DB write ≤ 50 ms |
BroadcastEmergency P99 | 3 000 ms | Budget guard in handler |
| Dispatch-to-finalisation P95 | 60 s | Aggregator sweeper 2 s cadence; per-MNO 30 s timeout |
| Per-MNO adapter send P95 | 5 s | Circuit breaker opens after 3 consecutive failures in 60 s |
| Drill cadence skew | ≤ 5 min from scheduled | Scheduler cron + backlog retry |
| Audit-chain verifier runtime | ≤ 30 min over 24 h partition | Parallelism 4 workers; checkpointed |
HSM C_Verify P95 | 300 ms | PKCS#11 session pool size 8 per pod |
3. Concurrency & Consistency
- Broadcast acceptance. Single-leader via Postgres row-version on
cbc.broadcasts; hash chain requires serial append within partition. - Dispatch workers. Multiple replicas; advisory lock
pg_try_advisory_xact_lockkeyed bybroadcastId. - Per-MNO dispatch. Independent per MNO; no ordering across MNOs.
- Cancel race. The
SETNXfirst-approver lock plus transactionalstatus ∈ {PENDING, DISPATCHED}check prevents cancel vs ack races: if the adapter acks while cancel is pending, the MNO's row progresses toACKEDand the cancel is markedineffectivefor that MNO. - Audit chain. Single append path —
DispatcherWorkerandBroadcastEmergencyhandler both serialize on the partition's advisory lock (pg_advisory_xact_lock(hashtext('cbc.audit.' || partition_name))) to guaranteeprevHashcorrectness.
4. Idempotency
| Operation | Key | TTL | Return |
|---|---|---|---|
BroadcastEmergency | cbc:idem:{requestId} | 24 h | Original broadcastId |
CancelBroadcast | cbc:cancel:pending:{broadcastId} | 60 s | Original approvalToken |
ScheduleDrill (cron) | cbc:drill:sched:{yyyy-mm-dd} | 1 day | Original drillId |
MnoCellDb refresh | cbc:celldb:{mnoId}:{yyyy-mm-dd} | 1 day | Original snapshotVersion |
Idempotency values are written to Redis before the DB transaction so a crash between Redis SETNX and DB commit leaves the request re-playable.
5. Ports (hexagonal adapters)
| Port | Inbound/Outbound | Implementations |
|---|---|---|
GrpcBroadcastPort | Inbound | gRPC server on :50061 (mTLS) |
RestAdminPort | Inbound | HTTPS on :3061 fronted by Kong |
HsmPort | Outbound | PKCS#11 via pkcs11.so sidecar (softhsm2 locally; Thales Luna / nCipher nShield in prod) |
CbeAdapterPort | Outbound | Standard3gppCbeAdapter, EricssonProprietaryCbeAdapter, HuaweiProprietaryCbeAdapter (one per MNO) |
NatsPublisherPort | Outbound | JetStream on CBC_EVENTS stream |
VaultSecretsPort | Outbound | Vault AppRole; per-MNO CBE creds at secret/cbc/mno/{mno-id}/cbe-credentials |
OcspCrlPort | Outbound | OCSP stapling verifier + CRL fetch cached in Redis 4 h |
ObjectStorePort | Outbound | S3-compatible (MinIO / sovereign) — drill reports, PKI cert archive |
Ports are thin interfaces; core domain logic depends only on ports, not implementations (swap CbeAdapterPort impl per MNO without touching domain code).
6. Rule Evaluation & Priority Lanes
CBC broadcasts bypass the compliance-engine evaluation pipeline (no tenant-content policy applies to authorised government broadcasts; ADR-0004 §8 places CBC on the P0 lane). The service enforces its own minimal gating:
- Authorised-caller registry check (UC-01 step 5–6).
- HSM-bound signature verify (UC-01 step 4).
- Anti-replay nonce window (UC-01 step 2).
- Language coverage for severity (DOMAIN §2.1.1).
- Geographic target within
caller.allowedRegions.
No further content-policy evaluation is performed. The regulator accepts this by virtue of the mouRef governing each AuthorisedCaller.
7. Release (cancellation) pathway
Cancellation is authoritative — once accepted, the broadcast transitions to CANCELLED and the NOC + regulator consumers receive cbc.broadcast.cancelled.v1. Re-submitting the "same" broadcast must use a new requestId; there is no "un-cancel" semantic.
8. Drill vs Real Broadcast at Runtime
Drills share 100% of the hot path with real emergencies — same handler, same worker, same adapters, same audit. The only differences, enforced at code-level by the domain layer:
isDrill = true⇒ CBS Message Identifier constrained to[4373, 4379].isDrill = true⇒ every language variant body is prepended withlocalisedDrillPrefix(lang)(UC-06).isDrill = true⇒cbc.audit.v1carriestransition = DRILL_EXECUTEDinstead ofDISPATCHED;cbc.drill.*events emitted in addition.isDrill = true⇒ no ATRA-reporting events (regulator receives drills via a separatecbc.drill.completed.v1report).