cbc-bridge-service — Domain Model
Version: 1.0 Status: Draft Owner: Government / Emergency Last Updated: 2026-04-21
Companion: SERVICE_OVERVIEW · APPLICATION_LOGIC · DATA_MODEL · EVENT_SCHEMAS · API_CONTRACTS · SECURITY_MODEL Related ADR: ADR-0004 §3 bounded contexts, §11 HSM key custody, §14 multi-region posture
1. Bounded Context
Civil Emergency Cell-Broadcast. The cbc-bridge-service owns every authoritative decision about whether, when, and how an emergency cell-broadcast reaches Afghan handsets — plus the regulator-grade, hash-chained evidence trail that proves it. It is a peer to sms-firewall-service (inbound perimeter), compliance-engine (outbound tenant SMS policy), and consent-ledger-service (subscriber consent). It is not an A2P SMS service and shares nothing with the smpp-connector data plane: cell-broadcast is a distinct 3GPP channel (3GPP TS 23.041 CBS / ETSI EN 302 117) carried by the MNO's Cell Broadcast Entity (CBE), not by MAP/SRI/SubmitSM.
The context boundary is drawn such that:
- Inside the boundary: emergency-broadcast requests, translation into 3GPP TS 23.041 CBS message PDUs, per-MNO CBE dispatch via vendor adapters, per-MNO acknowledgement aggregation, drill broadcasts, the authorised-caller registry, per-MNO cell-database snapshots for geographic targeting, government-PKI signature-verification evidence, and the hash-chained broadcast audit.
- Outside the boundary: issuance of the national-PKI itself (held by NDMA / ATRA), subscriber-handset behaviour (out of our control — CBS has no per-subscriber delivery receipt), A2P SMS dispatch (owned by
smpp-connector-poolper ADR-0004 §7), map authoring UI (owned byregulator-portal-serviceper EP-REG-01), human translation authoring (owned bycustomer-portallocalisation glossary per EP-CUST-09), multi-channel non-emergency notifications (owned bynotification-serviceper EP-NOTIF-07).
The service consumes PKI certificate trust material from the HSM and cell-tower coordinate exports from MNOs, but never persists authoritative state for either — it persists only its own verdicts, dispatches, and the evidence underpinning them.
2. Aggregates
2.1 Broadcast
The authoring unit of an emergency cell-broadcast. One Broadcast is one authorised request from a government caller, in one to four language variants, with one geographic target, scheduled for immediate or near-immediate dispatch.
| Field | Type | Notes |
|---|---|---|
broadcastId | UUIDv4 | Identity (external prefix bc_) |
callerId | UUIDv4 | FK → AuthorisedCaller.callerId |
headline | string ≤ 60 chars | Short headline; required |
bodyVariants | LanguageVariant[] | 1–4 entries keyed by language code; invariants in §2.1.1 |
geoTarget | GeoTarget VO | Cell-ID list / polygon / region / country |
severity | enum Severity | P0_EXTREME · P1_MAJOR · P2_ADVISORY |
cbsMessageIdentifier | int | 4370 (P0) / 4371 (P1) / 4372 (P2) / 4373–4379 (test/drill slot) |
isDrill | boolean | True when broadcast is a drill per CBC-US-015 |
serialNumber | int 16-bit | 3GPP TS 23.041 §9.4.1.2 Serial Number (GeoScope + Msg Code + Update Number) |
expiresAt | TIMESTAMPTZ | Broadcast expiry; ≤ acceptedAt + 2h for P0 |
state | enum BroadcastState | ACCEPTED → DISPATCHING → ACKED | PARTIAL | FAILED | CANCELLED |
signatureAuditId | UUIDv4 | FK → SignatureAudit.auditId (verifies caller) |
prevHash | hex | SHA-256 of prior broadcast row in partition (hash chain) |
recordHash | hex | SHA-256(canonicalJson(row minus recordHash) ‖ prevHash) |
acceptedAt, dispatchedAt, finalisedAt | TIMESTAMPTZ | Lifecycle |
2.1.1 Invariants
- PKI signature mandatory. A
Broadcastis never persisted without a successfulSignatureAuditrow recorded via HSM (PKCS#11C_Verify). In-process verification with a file-mounted public key is prohibited at the code level and enforced by a start-up guard (ADR-0004 §11). - Caller-authorisation mandatory.
callerIdMUST resolve to anAuthorisedCallerwhoseallowedSeveritiescontainsseverity, whoseallowedRegionsspatially containsgeoTarget, and whose validity window (notBefore ≤ now ≤ notAfter) coversacceptedAt. Failure yieldsPERMISSION_DENIED. - Language coverage.
P0_EXTREMErequiresen+fa(Dari) +ps(Pashto).P1_MAJORrequiresen+ at least one offa/ps.P2_ADVISORYrequiresen. - Mutually exclusive states. State transitions are one-way:
ACCEPTED → DISPATCHING → {ACKED | PARTIAL | FAILED | CANCELLED}.CANCELLEDreachable only before per-MNOACKEDis observed for any MNO. - Immutable after
DISPATCHING. Onlystate,finalisedAt, and per-MNO dispatch references may change once dispatch has started.bodyVariants,geoTarget,severity, andcbsMessageIdentifierare frozen. - Test-range Message Identifier for drills.
isDrill = trueimpliescbsMessageIdentifier ∈ [4373, 4379](3GPP TS 23.041 Annex A designated test slot).isDrill = falseimpliescbsMessageIdentifier ∈ [4370, 4372]. - Serial-number monotonicity. Per
(mnoId, cbsMessageIdentifier, geoScope)the Update Number field of the Serial Number MUST be strictly monotonic to allow handsets to dedup per 3GPP TS 23.041 §9.4.1.2.2.
2.2 MnoDispatch
A per-MNO child of a Broadcast representing a single attempt to dispatch the CBS PDU(s) to one MNO CBE endpoint.
| Field | Type | Notes |
|---|---|---|
dispatchId | UUIDv4 | External prefix disp_ |
broadcastId | UUIDv4 | FK |
mnoId | enum | AWCC · Roshan · Etisalat · MTN_AF · Salaam (extensible) |
adapterKind | enum AdapterKind | STANDARD_3GPP · ERICSSON_PROPRIETARY · HUAWEI_PROPRIETARY |
resolvedCellIds | string[] | Cell-ID list produced by GeoTarget resolution using per-MNO CellDatabase |
cbsPdus | JSONB | Encoded per-language CBS PDUs (one per language variant); stored for regulator audit |
attempt | int | 1-based; increments on adapter retry |
status | enum DispatchStatus | PENDING · DISPATCHED · ACKED · FAILED · TIMEOUT · REJECTED |
cbeAckReference | string | null | MNO-assigned ack ID (opaque) |
latencyMs | int | Request→ack latency |
errorCode, errorDetail | string | null | Populated on FAILED / REJECTED |
dispatchedAt, ackedAt | TIMESTAMPTZ | Lifecycle |
Invariants
- Exactly one
MnoDispatchper(broadcastId, mnoId)unless a retry supersedes a prior row (attempt > 1and prior row'sstatus ∈ {FAILED, TIMEOUT}). - Status transitions are one-way:
PENDING → DISPATCHED → {ACKED | FAILED | TIMEOUT | REJECTED}. Re-dispatch incrementsattemptand creates a new row; the prior row remains for audit. - Per-MNO timeout is 30 s (adapter-configurable per MNO contract).
2.3 AuthorisedCaller
A registered government client authorised to submit emergency broadcasts. Authorisation is explicit and auditable, not implicit-by-cert-validity.
| Field | Type | Notes |
|---|---|---|
callerId | UUIDv4 | External prefix caller_ |
orgName | string | e.g. NDMA, Afghan Civil Defence, Ministry of Interior |
certSubject | string | RFC 4514 DN of the allowed national-PKI client certificate |
certFingerprintSha256 | hex | Additional pin on the caller's end-entity cert |
allowedSeverities | enum Severity[] | e.g. [P1_MAJOR, P2_ADVISORY] — only NDMA may carry P0_EXTREME |
allowedRegions | string[] | Named provinces/districts or ISO-3166-2 codes (AF, AF-KAB, AF-BAL) |
mouRef | string | Inter-agency MOU reference (binds legal authority) |
notBefore, notAfter | TIMESTAMPTZ | Validity window |
dualControlPartners | UUIDv4[] | callerIds that may co-approve cancellations/severity-escalations |
active | boolean | Soft-disable without deletion |
createdAt, deactivatedAt | TIMESTAMPTZ | Lifecycle |
Invariants
(certSubject)is unique amongactive = TRUErows.- Mutations produce an append-only
authorised_callers_historyrow (never silently overwrite). - A caller MUST have
mouRef— broadcasts without legal basis are architecturally refused.
2.4 CellDatabase
A per-MNO snapshot of cell-tower coordinates used for polygon/region → Cell-ID resolution.
| Field | Type | Notes |
|---|---|---|
mnoId | enum | As §2.2 |
cellId | string | MNO-assigned Cell Global Identity (MCC+MNC+LAC+CI per 3GPP TS 23.003 §4.3.1) |
lat, lng | numeric(9,6) | WGS 84 |
accuracyMeters | int | From MNO export; used for polygon edge handling |
lastUpdatedAt | TIMESTAMPTZ | Per-row updated from weekly MNO export |
snapshotVersion | int | Per-MNO snapshot version; allows rollback to last-known-good |
Invariants
- A snapshot ingest is atomic per MNO: either the entire export is applied (with a new
snapshotVersion) or none. - A
GeoTargetpolygon is resolved at dispatch time against the current snapshot; the resolvedresolvedCellIdsis frozen into theMnoDispatchrow so audit evidence survives subsequent snapshot refreshes. - Coverage report per MNO:
% of national-area polygons resolvable— alert below 85% per-MNO.
2.5 Drill
A scheduled drill broadcast, distinct from a real emergency at the state-machine level.
| Field | Type | Notes |
|---|---|---|
drillId | UUIDv4 | External prefix drill_ |
broadcastId | UUIDv4 | null | Populated once the associated Broadcast row is created |
cadence | enum | MANUAL · MONTHLY_FIRST_TUESDAY · QUARTERLY |
scheduledAt | TIMESTAMPTZ | Next scheduled execution |
geoTarget | GeoTarget | Typically national |
bodyVariants | LanguageVariant[] | Drill body, prefixed with localised "DRILL — NO ACTION REQUIRED" |
afterActionReportRef | string | null | Object-store URI once dispatched |
state | enum | SCHEDULED · EXECUTED · SKIPPED_HOLIDAY · CANCELLED |
Invariants
- A
Drill's resultingBroadcastMUST carryisDrill = trueandcbsMessageIdentifierin the CBS test slot (4373..4379). - A
Drillnever emitsregulator.atra.*downstream events (drills are internal exercises). - Two consecutive missed scheduled drills trigger
CbcDrillOverdue(MEDIUM).
2.6 BroadcastAuditEntry
Append-only, hash-chained evidence record for every broadcast state transition (request, dispatch, ack, partial, failed, cancelled). This is the primary regulator-defensible evidence feed.
| Field | Notes |
|---|---|
auditId | UUIDv4 (external prefix audit_) |
broadcastId | FK |
transition | enum AuditTransition — REQUESTED · DISPATCHED · ACKED · PARTIAL · FAILED · CANCELLED · DRILL_EXECUTED |
prevHash | SHA-256 of the previous row in the partition |
rowHash | SHA-256(canonicalJson(row minus rowHash) ‖ prevHash) |
snapshot | JSONB — broadcast + per-MNO dispatches at this instant |
actorCallerId, actorApproverId | Who triggered the transition |
occurredAt | TIMESTAMPTZ (partition key, monthly RANGE) |
Invariants
- Append-only at the database level.
UPDATEandDELETErejected by Postgres rules (see DATA_MODEL §3.1). prevHashMUST equal the prior row'srowHashwithin the partition. A break is a CRITICAL alert (CbcAuditChainBroken).- Retention: ≥ 13 months hot, ≥ 7 years cold (regulator obligation per ADR-0004 §9).
2.7 SignatureAudit
Every national-PKI signature verification result — success or failure — is written here. This exists so probing, misconfiguration, and revocation effects are all observable.
| Field | Notes |
|---|---|
auditId | UUIDv4 |
presentedCertSubject | string (DN) |
presentedCertFingerprintSha256 | hex |
signatureAlgorithm | string (e.g. SHA256withRSA) |
result | enum VerifyResult — VERIFIED · SIGNATURE_INVALID · CERT_EXPIRED · CERT_REVOKED_CRL · CERT_REVOKED_OCSP · CALLER_NOT_REGISTERED · CALLER_SCOPE_VIOLATION · HSM_UNAVAILABLE |
pkcs11Operation | string (e.g. C_Verify) |
evidence | JSONB — OCSP response hash, CRL age, HSM slot id |
observedAt | TIMESTAMPTZ |
Invariants
- Append-only. Retention 25 months (covers two regulator review cycles).
3. Value Objects
| VO | Shape | Invariants |
|---|---|---|
Severity | enum P0_EXTREME · P1_MAJOR · P2_ADVISORY | P0_EXTREME requires NDMA-tier caller; P1→P0 escalation requires dual-approver per CBC-US-014 |
GeoTarget | `{ kind: CELL_IDS | POLYGON |
CbsMessageIdentifier | int | 4370=P0, 4371=P1, 4372=P2, [4373..4379]=test (3GPP TS 23.041 Annex A) |
LanguageVariant | `{ lang: 'en' | 'fa' |
CbsSerialNumber | 16-bit int | Bits 0–3 GeoScope, 4–13 Msg Code, 14–15 Update Number (3GPP TS 23.041 §9.4.1.2.2) |
CbsPduPage | { pageNo: int, pageCount: int, contentHex: string } | pageCount ≤ 15 per spec; pageNo ∈ [1..pageCount] |
BroadcastState | enum | ACCEPTED · DISPATCHING · ACKED · PARTIAL · FAILED · CANCELLED |
DispatchStatus | enum | PENDING · DISPATCHED · ACKED · FAILED · TIMEOUT · REJECTED |
CallerCertPin | { subject, sha256Fingerprint } | Used to pin the exact end-entity cert against the PKI chain |
4. Domain Events (produced)
Detailed JSON Schemas in EVENT_SCHEMAS.md. All events carry schemaVersion, eventId, traceId, at, and are emitted via the transactional outbox pattern.
| Event | Trigger | Core payload |
|---|---|---|
cbc.broadcast.requested.v1 | Verified BroadcastEmergency persists ACCEPTED row | broadcastId, callerId (masked on partner fan-out), severity, geoTarget, isDrill |
cbc.broadcast.dispatched.v1 | state transitions ACCEPTED → DISPATCHING | broadcastId, mnoIds[], expectedAckByMs |
cbc.broadcast.acked.v1 | Final state ACKED | broadcastId, perMno (mno → ACKED/FAILED/TIMEOUT), coveragePct |
cbc.broadcast.partial.v1 | Final state PARTIAL | As above, plus missingMnoIds[] |
cbc.broadcast.failed.v1 | Final state FAILED | broadcastId, perMno, reasonCode |
cbc.broadcast.cancelled.v1 | Dual-control cancellation accepted | broadcastId, initiatorCallerId, approverCallerId, effectiveCancellations, ineffectiveCancellations |
cbc.audit.v1 | Every BroadcastAuditEntry append | auditId, broadcastId, transition, rowHash |
cbc.drill.scheduled.v1 | Drill scheduled (cron or manual) | drillId, scheduledAt, geoTarget |
cbc.drill.completed.v1 | Drill's underlying broadcast finalised | drillId, broadcastId, reportRef |
cbc.mno.adapter.health.v1 | Adapter circuit-breaker opens or closes | mnoId, adapterKind, state (OPEN/CLOSED), reason |
cbc.signature.audit.v1 | Every SignatureAudit append (low-volume) | auditId, result, presentedCertSubject (masked below platform.cbc.admin) |
PII guardrails — broadcast bodies are public by design (everyone in the targeted area will read them), so bodies may appear in events. However, caller identity (cert subject) is internal-confidential and appears only on streams consumed by platform-internal subscribers; the public-facing status.ghasi.io/cbc-drills feed receives a redacted view.
5. Consumed Events
| Subject | Producer | Effect |
|---|---|---|
regulator.ca.trust.updated.v1 | regulator-portal-service | Reload the national-PKI trust chain into the HSM slot; refresh OCSP responder URL |
sender.id.suspended.v1 | sender-id-registry-service | Ignored (CBC is not a sender-ID channel); documented so cross-referencing reviewers don't expect it |
consent.dnd.snapshot.v1 | consent-ledger-service | NOT consumed. CBS is an emergency channel — DND does not apply (3GPP TS 23.041 §3 priority handling). Documented explicitly so reviewers don't expect filtering. |
fraud.detected.* | fraud-intel-service | Not consumed — fraud intel does not apply to inbound government broadcasts |
6. Global Invariants
- Fail-closed for authentication, fail-open per-MNO for dispatch. Every broadcast requires successful HSM-backed PKI signature verification (fail-closed). Once past authentication, dispatch fans out to all MNO CBEs in parallel; any individual MNO failure yields
PARTIAL(fail-open per-MNO) rather than blocking on the slowest MNO (CBC-US-004). - Append-only audit with hash-chain continuity.
cbc.broadcastsandcbc.auditreject UPDATE/DELETE at the DB level. A break in theprevHash → rowHashchain is a CRITICAL alert. - Dual-control on cancellation and severity escalation. Cancelling an in-flight broadcast or escalating P1→P0 requires a second
AuthorisedCaller.callerIdlisted in the initiator'sdualControlPartnerswithin a 60-second window (CBC-US-005, CBC-US-014). - Drill mode is first-class. Drills are distinguishable at every layer: CBS Message Identifier in test slot,
isDrill=trueonBroadcastrow,cbc.drill.*event subjects, no ATRA export, "DRILL — NO ACTION REQUIRED" localised prefix (CBC-US-015). - Replay-attack window. Each
BroadcastEmergencysignature is bound to anonce+requestedAt(RFC 3339, UTC). A signature is accepted only if|now - requestedAt| ≤ 120sand(presentedCertFingerprint, nonce)has not been seen in the last 24 h (Rediscbc:nonce:{fp}:{nonce}with 24 h TTL). - Body confidentiality is irrelevant for broadcast content (public by nature). Body confidentiality is relevant for the
AuthorisedCallerregistry (certFingerprint,mouRef) andSignatureAuditevidence (role-gated toplatform.cbc.admin/platform.auditor). - Per-MNO cell database is authoritative at dispatch time. The
resolvedCellIdsfrozen onMnoDispatchis the single source of truth for "which cells did this broadcast reach" — subsequent MNO cell-DB refreshes do not retroactively change audit evidence.
7. State Machines
7.1 Broadcast State Machine
┌───────────┐
BroadcastEmergency ──▶ │ ACCEPTED │
(HSM verified) └─────┬─────┘
│ dispatcher picks up
▼
┌──────────────┐
│ DISPATCHING │
└──┬─┬─┬─┬─────┘
│ │ │ │
ack-aggregator tallies per-MNO dispatches
│ │ │ │
┌──────ALL ACKED─────┐
│ │
▼ │
┌──────────┐ │
│ ACKED │ │
└──────────┘ │
│ mixed outcomes
▼
┌──────────┐
│ PARTIAL │
└──────────┘
│ all MNOs failed
▼
┌──────────┐
│ FAILED │
└──────────┘
dual-control CancelBroadcast (before any MNO ACKED)
│
▼
┌─────────────┐
│ CANCELLED │
└─────────────┘
7.2 MnoDispatch State Machine
PENDING ──dispatcher send──▶ DISPATCHED
│ │
│ ├─ CBE ack ──▶ ACKED
│ ├─ CBE nack ──▶ REJECTED
│ ├─ 30 s no-ack ──▶ TIMEOUT
│ └─ network err ─▶ FAILED
7.3 AuthorisedCaller State Machine
(admin create) (admin revoke) (notAfter passes)
─────────────▶ active ───────────────▶ deactivated ───▶ archived-history
│
CRL/OCSP revocation observed
▼
auto-disable (active=FALSE) + PagerDuty
8. Aggregate Lifecycle Summary
| Aggregate | Created by | Mutated by | Retired by |
|---|---|---|---|
Broadcast | BroadcastEmergency gRPC handler (after HSM verify) | Ack aggregator (state); dual-control cancel (state=CANCELLED) | Partition pruned 13 months hot / 7 years cold |
MnoDispatch | Dispatcher worker on DISPATCHING transition | Adapter on CBE response | Retained with parent; retry creates new row |
AuthorisedCaller | Platform admin REST (+ Legal sign-off of mouRef) | Admin edit (new history row appended) | Soft-delete (active=FALSE); never hard-deleted |
CellDatabase | Weekly MNO export ingest (or admin upload) | Atomic replace per MNO snapshot | Prior snapshots retained 90 d for rollback |
Drill | Cron scheduler or admin REST | Scheduler advances next scheduledAt after execution | Retained with underlying Broadcast row |
BroadcastAuditEntry | Append on every state transition | Immutable | Partition pruned per retention |
SignatureAudit | Append on every HSM verify attempt | Immutable | 25-month retention |
9. Cross-Service References
regulator-portal-serviceis the only non-government caller for some flows (regulator observation of drills per EP-REG-01). It is also the producer ofregulator.ca.trust.updated.v1that refreshes the national-PKI trust chain.customer-portalEP-CUST-09 owns the localisation glossary; drill/broadcast body template authoring in the admin UI pulls approved translations from the glossary.admin-dashboardEP-ADMDASH-10 hosts the regulator workbench surface forGET /v1/cbc/broadcastsand the hash-chain verifier view.notification-serviceEP-NOTIF-07 subscribes tocbc.broadcast.acked.v1for non-handset distribution (platform portal push + media-partner webhook of drill records per CBC-US-017).- ADR-0004 §11 binds the PKI verification path to the HSM (PKCS#11
C_Verify). §14 (multi-region posture) makes broadcasts region-local and audit globally mirrored — see SYNC_CONTRACT.md §5.
10. Open Points
| ID | Question | Owner |
|---|---|---|
| DM-OPEN-01 | Extension of AuthorisedCaller.allowedRegions to ISO-3166-2 for cross-border DR drills (Tajik / Uzbek partner exercises)? | Legal + NDMA |
| DM-OPEN-02 | Whether isDrill=true broadcasts should also populate cbc.audit.v1 or a separate cbc.drill.audit.v1 subject | Trust & Safety |
| DM-OPEN-03 | Whether Update-Number roll-over (2-bit → max 3 updates per (MsgCode, GeoScope)) requires a Msg Code auto-increment policy | Messaging Core |