cbc-bridge-service — Event Schemas
Version: 1.0 Status: Draft Owner: Government / Emergency Last Updated: 2026-04-21
Companion: DOMAIN_MODEL · SYNC_CONTRACT · SECURITY_MODEL
All events are published to NATS JetStream via the transactional outbox pattern. Every event carries schemaVersion, eventId (UUIDv4), traceId, and at (RFC 3339). Broadcast bodies may appear in events because emergency bodies are public by design; caller identity is internal-confidential and masked for non-platform consumers.
1. Streams, subjects, retention
| Stream | Subjects | Retention | Replicas | Deduplication window |
|---|---|---|---|---|
CBC_EVENTS | cbc.broadcast.requested.v1, cbc.broadcast.dispatched.v1, cbc.broadcast.acked.v1, cbc.broadcast.partial.v1, cbc.broadcast.failed.v1, cbc.broadcast.cancelled.v1 | 13 months (regulator) | 3 (kbl + mzr + dxb leaf) | 2 min |
CBC_AUDIT | cbc.audit.v1 | 13 months hot, 7 years cold | 3 | 5 min |
CBC_DRILL | cbc.drill.scheduled.v1, cbc.drill.completed.v1 | 25 months | 3 | 2 min |
CBC_SIGNATURE | cbc.signature.audit.v1 | 25 months | 3 | 2 min |
CBC_HEALTH | cbc.mno.adapter.health.v1, cbc.mno.cell.refreshed.v1 | 30 days | 3 | — |
All streams have a .deadletter suffix. Consumers are durable with explicit ack per PLT-REQ-008/009.
Cross-region replication: CBC_EVENTS and CBC_AUDIT replicate kbl → mzr synchronously (stream mirror) and kbl → dxb asynchronously (leaf-node audit-only mirror) per ADR-0004 §5.
2. Produced events
2.1 cbc.broadcast.requested.v1
Emitted on successful acceptance of BroadcastEmergency.
{
"$id": "https://schemas.ghasi.io/cbc/broadcast-requested.v1.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["schemaVersion","eventId","broadcastId","callerRef","severity","isDrill","geoTarget","acceptedAt","traceId","at"],
"properties": {
"schemaVersion": { "const": "1" },
"eventId": { "type": "string", "format": "uuid" },
"broadcastId": { "type": "string", "pattern": "^bc_[A-Z0-9]+$" },
"callerRef": {
"type": "object",
"properties": {
"orgName": { "type": "string" },
"callerId": { "type": "string", "pattern": "^caller_[A-Z0-9]+$" },
"certSubject": { "type": "string", "description": "masked on public/partner fan-out" }
}
},
"severity": { "enum": ["P0_EXTREME","P1_MAJOR","P2_ADVISORY"] },
"isDrill": { "type": "boolean" },
"cbsMessageIdentifier": { "type": "integer" },
"geoTarget": {
"oneOf": [
{ "type": "object", "properties": { "kind": { "const": "CELL_IDS" }, "cellCount": { "type": "integer" } } },
{ "type": "object", "properties": { "kind": { "const": "POLYGON" }, "areaKm2": { "type": "number" } } },
{ "type": "object", "properties": { "kind": { "const": "REGION" }, "regionCode": { "type": "string" } } },
{ "type": "object", "properties": { "kind": { "const": "COUNTRY" } } }
]
},
"languageCodes": { "type": "array", "items": { "enum": ["en","fa","ps","ar"] } },
"expectedDispatchBy": { "type": "string", "format": "date-time" },
"acceptedAt": { "type": "string", "format": "date-time" },
"signatureAuditId":{ "type": "string", "format": "uuid" },
"traceId": { "type": "string" },
"at": { "type": "string", "format": "date-time" }
}
}
2.2 cbc.broadcast.dispatched.v1
{
"schemaVersion": "1",
"eventId": "uuid",
"broadcastId": "bc_...",
"mnoIds": ["AWCC","Roshan","Etisalat","MTN_AF","Salaam"],
"adapterKinds": { "AWCC":"STANDARD_3GPP", "MTN_AF":"ERICSSON_PROPRIETARY", "Etisalat":"HUAWEI_PROPRIETARY" },
"cellCountsPerMno": { "AWCC": 1240, "Roshan": 980 },
"dispatchedAt": "RFC3339",
"expectedAckBy": "RFC3339",
"traceId": "...", "at": "..."
}
2.3 cbc.broadcast.acked.v1 / .partial.v1 / .failed.v1
Shared shape:
interface CbcBroadcastFinal {
schemaVersion: '1';
eventId: string;
broadcastId: string;
finalState: 'ACKED' | 'PARTIAL' | 'FAILED';
perMno: Array<{
mnoId: string;
status: 'ACKED'|'FAILED'|'TIMEOUT'|'REJECTED';
adapterKind: 'STANDARD_3GPP'|'ERICSSON_PROPRIETARY'|'HUAWEI_PROPRIETARY';
cellCount: number;
latencyMs: number;
cbeAckReference?: string;
errorCode?: string;
}>;
missingMnoIds: string[]; // non-empty for PARTIAL/FAILED
coveragePct: number; // 0.0..1.0
finalisedAt: string; // RFC 3339
traceId: string;
at: string;
}
2.4 cbc.broadcast.cancelled.v1
interface CbcBroadcastCancelled {
schemaVersion: '1';
eventId: string;
broadcastId: string;
initiatorCallerId: string;
approverCallerId: string;
effectiveCancellations: string[]; // mno ids where cancel reached before ACK
ineffectiveCancellations: string[]; // mno ids that had already ACKED
cancelledAt: string;
traceId: string;
at: string;
}
2.5 cbc.audit.v1
The primary regulator-defensibility feed. One event per state transition (REQUESTED, DISPATCHED, ACKED, PARTIAL, FAILED, CANCELLED, DRILL_EXECUTED).
interface CbcAudit {
schemaVersion: '1';
eventId: string;
auditId: string; // audit_...
broadcastId: string;
transition: 'REQUESTED'|'DISPATCHED'|'ACKED'|'PARTIAL'|'FAILED'|'CANCELLED'|'DRILL_EXECUTED';
prevHash: string; // sha256 hex
rowHash: string; // sha256 hex
snapshotHash: string; // sha256 of canonical JSON of the snapshot (full snapshot not in event — fetched via REST)
actorCallerId: string | null;
actorApproverId: string | null;
occurredAt: string;
traceId: string;
at: string;
}
Why snapshot hash not snapshot: keeps cbc.audit.v1 compact for high-throughput verification; full snapshot is retrievable via GET /v1/cbc/broadcasts/{id}/audit for regulator review.
2.6 cbc.drill.scheduled.v1
interface CbcDrillScheduled {
schemaVersion: '1';
eventId: string;
drillId: string;
cadence: 'MANUAL'|'MONTHLY_FIRST_TUESDAY'|'QUARTERLY';
scheduledAt: string;
geoTarget: { kind: string; /* payload */ };
languageCodes: string[];
traceId: string;
at: string;
}
2.7 cbc.drill.completed.v1
interface CbcDrillCompleted {
schemaVersion: '1';
eventId: string;
drillId: string;
broadcastId: string;
finalState: 'ACKED'|'PARTIAL'|'FAILED';
perMnoLatencyMsP95: number;
cellCountResolved: number;
partialFailureBreakdown: Array<{ mnoId: string; failureCode: string }>;
reportRef: string; // object-store URI of PDF/HTML after-action
traceId: string;
at: string;
}
2.8 cbc.mno.adapter.health.v1
Circuit-breaker transition.
interface CbcMnoAdapterHealth {
schemaVersion: '1';
eventId: string;
mnoId: 'AWCC'|'Roshan'|'Etisalat'|'MTN_AF'|'Salaam';
adapterKind: 'STANDARD_3GPP'|'ERICSSON_PROPRIETARY'|'HUAWEI_PROPRIETARY';
state: 'OPEN' | 'HALF_OPEN' | 'CLOSED';
reason: string; // e.g. '3 consecutive C_Send failures in 60s'
openedAt?: string;
closedAt?: string;
traceId: string;
at: string;
}
2.9 cbc.mno.cell.refreshed.v1
Emitted after a successful per-MNO cell-database atomic swap (UC-09).
interface CbcMnoCellRefreshed {
schemaVersion: '1';
eventId: string;
mnoId: string;
snapshotVersion: number;
rowCount: number;
coveragePctEstimate: number; // derived from synthetic test polygons
refreshedAt: string;
traceId: string;
at: string;
}
2.10 cbc.signature.audit.v1
Emitted for every HSM verify attempt (low-volume, high-value for security forensics).
interface CbcSignatureAudit {
schemaVersion: '1';
eventId: string;
auditId: string;
result:
| 'VERIFIED' | 'SIGNATURE_INVALID'
| 'CERT_EXPIRED' | 'CERT_REVOKED_CRL' | 'CERT_REVOKED_OCSP'
| 'CALLER_NOT_REGISTERED' | 'CALLER_SCOPE_VIOLATION'
| 'HSM_UNAVAILABLE';
presentedCertSubjectMasked: string; // 'CN=****.gov.af'; full only on internal audit channels
pkcs11Operation: 'C_Verify';
sourceIp: string;
observedAt: string;
traceId: string;
at: string;
}
3. Consumed events
| Subject | Producer | Effect |
|---|---|---|
regulator.ca.trust.updated.v1 | regulator-portal-service | Reload national-PKI trust chain into HSM slot; refresh OCSP URL (per DOMAIN §5) |
sender.id.suspended.v1 | sender-id-registry-service | Not consumed — documented for reviewers |
consent.dnd.snapshot.v1 | consent-ledger-service | Not consumed — CBS is emergency channel (3GPP TS 23.041 §3) |
No domain event from outside triggers a broadcast. The service is strictly request-driven.
4. Consumer contracts (by subscribing service)
| Service | Subjects | Action |
|---|---|---|
notification-service (EP-NOTIF-07) | cbc.broadcast.acked.v1, cbc.broadcast.partial.v1, cbc.broadcast.failed.v1, cbc.drill.completed.v1 | Platform portal push for government-client admins; optional webhook fan-out to media partners (CBC-US-017) |
analytics-service | cbc.audit.v1, cbc.broadcast.*.v1, cbc.drill.*.v1, cbc.signature.audit.v1 | Long-term analytics + compliance dashboards |
admin-dashboard (SSE) | cbc.broadcast.*.v1, cbc.drill.*.v1, cbc.mno.adapter.health.v1 | Live regulator workbench |
regulator-portal-service | cbc.audit.v1, cbc.drill.completed.v1 | Regulator evidence feed |
cbc-bridge-service (self) | cbc.adapter.ack.internal.v1 (internal-only stream) | UC-04 ack aggregation |
5. PII & security rules for events
- Broadcast bodies are public by design — included in events on platform-internal streams. Public feeds (CBC-US-017) receive a redacted envelope without caller identity.
- Caller identity is
CONFIDENTIAL-INTERNAL. Subjects beyondplatform-internalmust usepresentedCertSubjectMasked(CN=****.gov.af). - PKI fingerprints, OCSP responses, HSM slot IDs never appear in any event; they live only in
cbc.signature_audittable. - Event signing. Every event payload is signed with the platform RS256 key (via JetStream publisher); critical consumers (
notification-service,regulator-portal-service) opportunistically verify. - Replay safety.
eventIdis unique; consumers deduplicate by(stream, subject, eventId).
6. Outbox pattern
All state changes write an event row to cbc.outbox in the same PostgreSQL transaction as the aggregate change (e.g. the cbc.broadcasts insert). A relay (CbcOutboxRelay worker) publishes to NATS with retry and explicit ack. This guarantees no emitted event without a persisted state change and no unpublished state change beyond a few seconds.
CREATE TABLE cbc.outbox (
event_id UUID PRIMARY KEY,
subject TEXT NOT NULL,
payload JSONB NOT NULL,
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ix_cbc_outbox_unpublished
ON cbc.outbox (created_at) WHERE published_at IS NULL;
7. Retention summary
| Subject | Retention class | Reason |
|---|---|---|
cbc.audit.v1 | Hot 13 m + Cold 7 y (ADR-0004 §9) | Regulator-defensibility |
cbc.broadcast.*.v1 | 13 m | Operational replay + regulator |
cbc.drill.*.v1 | 25 m | Cross-review cycle |
cbc.signature.audit.v1 | 25 m | Forensic window |
cbc.mno.adapter.health.v1 | 30 d | Operational |
cbc.mno.cell.refreshed.v1 | 30 d | Operational |
Retention is managed by JetStream stream configuration + cold archive to MinIO (S3-compatible) for CBC_AUDIT older than 13 months.
8. NATS subject taxonomy (canonical)
cbc.broadcast.requested.v1
cbc.broadcast.dispatched.v1
cbc.broadcast.acked.v1
cbc.broadcast.partial.v1
cbc.broadcast.failed.v1
cbc.broadcast.cancelled.v1
cbc.audit.v1
cbc.audit.chain.verified.v1 (UC-07 success)
cbc.audit.chain.broken.v1 (UC-07 failure — CRITICAL)
cbc.drill.scheduled.v1
cbc.drill.completed.v1
cbc.signature.audit.v1
cbc.mno.adapter.health.v1
cbc.mno.cell.refreshed.v1
cbc.adapter.ack.internal.v1 (internal only — not replicated to dxb)
9. Schema evolution
- Additive fields with non-required defaults are non-breaking within the same
schemaVersion. - Breaking changes bump to
cbc.<topic>.v2. Subjects coexist during a deprecation window (≥ 90 days); subscribers migrate offv1before it is removed. - Enum values are additive. Consumers MUST treat unknown enum values as
UNKNOWNand not fail.