numbering-service — Event Schemas
Version: 1.0 Status: Draft Owner: Commerce Engineering + Platform Engineering Last Updated: 2026-04-21 Companion: DOMAIN_MODEL · SYNC_CONTRACT · SECURITY_MODEL
All events are published to NATS JetStream via the transactional outbox pattern (see SYNC_CONTRACT — Outbox pattern). Every event carries schemaVersion, eventId (UUIDv4), traceId, at (RFC 3339), and a regionId tag for multi-region fan-out per ADR-0004 §14. Identifier values are inventory assets, not PII (MSISDNs at the platform level are not tied to a subscriber) but are still classified INTERNAL and transmitted only on authenticated NATS connections.
1. Streams, Subjects, Retention
| Stream | Subjects | Retention | Replicas | Dedup window |
|---|---|---|---|---|
NUMBERING_EVENTS | number.reserved.v1, number.released.v1, number.assigned.v1, number.renewed.v1, number.suspended.v1, number.reinstated.v1, number.recalled.v1, number.quarantine.started.v1, number.quarantine.completed.v1 | 13 months hot | 3 | 2 min |
NUMBERING_AUDIT | numbering.audit.v1 | 13 months hot + 7 years cold (S3 object-lock) (regulatory per ATRA MoU) | 3 | 2 min |
NUMBERING_LEASES | number.lease.imported.v1, number.lease.batch.completed.v1 | 7 years (regulatory) | 3 | 2 min |
NUMBERING_OPS | number.conflict.detected.v1, number.pool.exhausted.v1, number.renewal.failed.v1 | 90 days | 3 | 2 min |
NUMBERING_REGULATOR | numbering.regulator.export.generated.v1 | 7 years | 3 | — |
All streams have a .deadletter suffix. Consumers are durable with explicit ack per PLT-REQ-008/009.
2. Produced Events
2.1 number.reserved.v1
Emitted on AVAILABLE → RESERVED and on RESERVED → HELD.
interface NumberReserved {
schemaVersion: '1';
eventId: string;
numberId: string;
value: string; // E.164 / short-code / alpha
type: 'MSISDN' | 'SHORT_CODE' | 'ALPHA_ID';
subtype: 'STANDARD' | 'VANITY' | 'TOLL_FREE' | 'PREMIUM_RATE' | 'MNO_INTERNAL';
tenantId: string;
reservationId: string;
kind: 'RESERVE' | 'HOLD';
expiresAt: string; // RFC 3339
operatorId: string | null; // MSISDN only
mcc: string | null; // '412' for Afghanistan
mnc: string | null;
actorUserId: string | null;
regionId: 'kbl' | 'mzr';
traceId: string;
at: string;
}
2.2 number.released.v1
Emitted when a reservation/hold is released without assignment (TTL or explicit) OR when a post-quarantine number returns to pool (in which case reason = QUARANTINE_COMPLETED).
interface NumberReleased {
schemaVersion: '1';
eventId: string;
numberId: string;
value: string;
type: 'MSISDN' | 'SHORT_CODE' | 'ALPHA_ID';
reservationId: string | null; // null for post-quarantine release
tenantId: string | null;
reason: 'TTL_EXPIRED' | 'TENANT_RELEASE' | 'PROMOTED_TO_LEASE' | 'PROMOTED_TO_HOLD' | 'QUARANTINE_COMPLETED' | 'ADMIN_OVERRIDE';
regionId: 'kbl' | 'mzr';
traceId: string;
at: string;
}
2.3 number.assigned.v1
Primary downstream-integration event. Consumed by billing-service (starts billing), sender-id-registry-service (marks alpha value inventory-committed), analytics-service, customer-portal.
interface NumberAssigned {
schemaVersion: '1';
eventId: string;
numberId: string;
value: string;
type: 'MSISDN' | 'SHORT_CODE' | 'ALPHA_ID';
subtype: 'STANDARD' | 'VANITY' | 'TOLL_FREE' | 'PREMIUM_RATE' | 'MNO_INTERNAL';
tenantId: string;
accountId: string | null;
leaseId: string;
term: 'P7D' | 'P30D' | 'P90D' | 'P1Y' | 'P3Y';
effectiveFrom: string;
effectiveUntil: string;
autoRenew: boolean;
vanityFlag: boolean;
operatorId: string | null;
mcc: string | null;
mnc: string | null;
leaseContractId: string | null;
previousLeaseId: string | null; // set on renewal
regionId: 'kbl' | 'mzr';
traceId: string;
at: string;
}
2.4 number.renewed.v1
interface NumberRenewed {
schemaVersion: '1';
eventId: string;
numberId: string;
value: string;
type: 'MSISDN' | 'SHORT_CODE' | 'ALPHA_ID';
tenantId: string;
previousLeaseId: string;
newLeaseId: string;
effectiveFrom: string;
effectiveUntil: string;
term: 'P30D' | 'P90D' | 'P1Y' | 'P3Y';
traceId: string;
at: string;
}
2.5 number.suspended.v1 / number.reinstated.v1
interface NumberSuspended {
schemaVersion: '1';
eventId: string;
numberId: string;
value: string;
type: 'MSISDN' | 'SHORT_CODE' | 'ALPHA_ID';
tenantId: string;
leaseId: string;
reason: 'REGULATOR_ORDER' | 'NON_PAYMENT' | 'ABUSE' | 'PLATFORM_HOLD';
ticketId: string | null;
actorUserId: string | null;
actorService: string | null;
traceId: string;
at: string;
}
interface NumberReinstated {
schemaVersion: '1';
eventId: string;
numberId: string;
value: string;
type: 'MSISDN' | 'SHORT_CODE' | 'ALPHA_ID';
tenantId: string;
leaseId: string;
reason: string;
ticketId: string;
actorUserId: string;
traceId: string;
at: string;
}
2.6 number.recalled.v1
interface NumberRecalled {
schemaVersion: '1';
eventId: string;
numberId: string;
value: string;
type: 'MSISDN' | 'SHORT_CODE' | 'ALPHA_ID';
tenantId: string;
leaseId: string;
reason: 'REGULATOR_ORDER' | 'ABUSE' | 'NON_PAYMENT' | 'TENANT_RELEASE' | 'EXPIRED' | 'PLATFORM_RECALL';
ticketId: string | null;
actorUserId: string | null;
actorService: string | null;
effectiveFrom: string; // original lease start
terminatedAt: string;
quarantineUntil: string | null; // null for alpha-ID (no cool-off)
traceId: string;
at: string;
}
2.7 number.quarantine.started.v1 / number.quarantine.completed.v1
interface NumberQuarantineStarted {
schemaVersion: '1';
eventId: string;
numberId: string;
value: string;
type: 'MSISDN' | 'SHORT_CODE';
previousTenantId: string;
recallReason: string;
quarantineFrom: string;
quarantineUntil: string;
cooloffDays: number; // 90 MSISDN / 30 short / 365 vanity
traceId: string;
at: string;
}
interface NumberQuarantineCompleted {
schemaVersion: '1';
eventId: string;
numberId: string;
value: string;
type: 'MSISDN' | 'SHORT_CODE';
completedAt: string;
completedBy: 'SWEEP_CRON' | 'ADMIN_OVERRIDE';
overrideBy: string | null;
overrideJustification: string | null;
traceId: string;
at: string;
}
2.8 number.lease.imported.v1 / number.lease.batch.completed.v1
Per-batch summary (not per-row — rows can reach 100 k+).
interface NumberLeaseImported {
schemaVersion: '1';
eventId: string;
batchId: string;
operatorId: string;
leaseContractId: string;
prefix: string; // e.g. '+9370'
imported: number;
duplicates: number;
invalid: number;
fileSha256: string;
signatureValid: boolean;
importedBy: string; // admin user id
traceId: string;
at: string;
}
interface NumberLeaseBatchCompleted {
schemaVersion: '1';
eventId: string;
batchId: string;
operatorId: string;
status: 'COMPLETED' | 'COMPLETED_WITH_ERRORS' | 'FAILED';
totalRows: number;
durationMs: number;
errorCount: number;
errorsRef: string | null; // S3 URI to invalid-row report
traceId: string;
at: string;
}
2.9 number.conflict.detected.v1
Produced when CAS fails (concurrent reserve) OR nightly reconciliation finds an orphan lease / prefix-overlap.
interface NumberConflictDetected {
schemaVersion: '1';
eventId: string;
kind: 'CAS_RACE' | 'ORPHAN_LEASE' | 'MISSING_LEASE' | 'PREFIX_OVERLAP' | 'CROSS_REGION_DIVERGENCE';
numberId: string | null;
value: string | null;
type: 'MSISDN' | 'SHORT_CODE' | 'ALPHA_ID' | null;
conflictingTenantIds: string[] | null;
detectedBy: 'RUNTIME_CAS' | 'RECONCILIATION_CRON' | 'MULTI_REGION_SYNC';
details: Record<string, unknown>;
traceId: string;
at: string;
}
Consumed by fraud-intel-service (feeds reputation signals) and surfaced on the platform SRE dashboard.
2.10 number.pool.exhausted.v1
interface NumberPoolExhausted {
schemaVersion: '1';
eventId: string;
scope: 'MNO_BLOCK' | 'SHORT_CODE_RANGE' | 'VANITY_RANGE' | 'TENANT_POOL';
operatorId: string | null;
blockPrefix: string | null;
tenantId: string | null;
total: number;
remaining: number;
remainingPct: number;
threshold: number; // 0.05 = 5%
severity: 'WARNING' | 'CRITICAL';
traceId: string;
at: string;
}
2.11 number.renewal.failed.v1
interface NumberRenewalFailed {
schemaVersion: '1';
eventId: string;
numberId: string;
value: string;
type: 'MSISDN' | 'SHORT_CODE' | 'ALPHA_ID';
tenantId: string;
leaseId: string;
reason: 'BILLING_REJECTED' | 'TENANT_SUSPENDED' | 'CONTRACT_EXPIRED' | 'QUOTA_ZERO';
expiresAt: string;
gracePeriodUntil: string | null; // for vanity leases: 14-day grace
traceId: string;
at: string;
}
2.12 numbering.audit.v1
Every lifecycle transition emits an audit mirror. This is the regulatory evidence feed and backs the hash-chained numbering.audit table.
interface NumberingAudit {
schemaVersion: '1';
eventId: string;
auditId: string;
numberId: string;
valueHashed: string; // sha256(value) — value itself not replicated to analytics
type: 'MSISDN' | 'SHORT_CODE' | 'ALPHA_ID';
fromState: string;
toState: string;
reasonCode: string;
actorUserId: string | null;
actorService: string | null;
tenantId: string | null;
leaseIdRef: string | null;
reservationIdRef: string | null;
quarantineIdRef: string | null;
prevHashHex: string;
rowHashHex: string;
occurredAt: string;
traceId: string;
at: string;
}
2.13 numbering.regulator.export.generated.v1
interface NumberingRegulatorExportGenerated {
schemaVersion: '1';
eventId: string;
exportId: string;
periodYearMonth: string; // 'YYYY-MM'
s3Ref: string;
sha256Hex: string;
signatureRef: string;
rowCount: number;
generatedAt: string;
traceId: string;
at: string;
}
Consumed by regulator-portal-service to surface the file to ATRA.
3. Consumed Events
| Subject | Producer | Action |
|---|---|---|
compliance.tenant.suspended.v1 | compliance-engine | Invoke UC-15 BulkRecallByTenant with reason: ABUSE, ticketId = complianceCaseId |
billing.account.delinquent.v1 | billing-service | Transition tenant leases to SUSPENDED; after 14-day grace → RECALLED |
billing.account.paid.v1 | billing-service | Reinstate SUSPENDED leases where grace window has not elapsed |
senderid.revoked.v1 | sender-id-registry-service | Recall the alpha-ID lease (reason: ABUSE with ticket reference) |
tenant.deleted.v1 | auth-service | Release all reservations + recall all leases (reason: PLATFORM_RECALL) |
mno.contract.updated.v1 | regulator-portal-service | Update LeaseContract.effectiveUntil or status |
tenant.plan.changed.v1 | billing-service | Refresh TenantPool quotas within 60 s |
fraud.signal.v1 (kind = ASSIGNMENT_BURST) | fraud-intel-service | Advisory — triggers per-tenant soft-rate-limit on Reserve |
4. Consumer Contracts (by service)
| Service | Subscribes to | Action |
|---|---|---|
billing-service | number.assigned.v1, number.released.v1, number.renewed.v1, number.recalled.v1 | Lifecycle billing (start / stop / renew / proration) |
sender-id-registry-service | number.assigned.v1 (type=ALPHA_ID), number.recalled.v1 (type=ALPHA_ID) | Keep sender-ID registry in lock-step with inventory truth |
sms-orchestrator | number.assigned.v1, number.recalled.v1, number.suspended.v1 | Cache invalidation for ValidateLease |
analytics-service | numbering.audit.v1, number.*.v1 | Long-term archival + pool-utilisation dashboards |
notification-service | number.pool.exhausted.v1, number.renewal.failed.v1 | Alert platform ops + tenant portal notifications |
fraud-intel-service | number.conflict.detected.v1, number.reserved.v1 (for burst detection) | Reputation inputs |
regulator-portal-service | numbering.regulator.export.generated.v1 | ATRA submission workflow |
5. PII & Security Rules for Events
- Raw MSISDNs are INTERNAL, not RESTRICTED — in platform context the MSISDN is an inventory asset leased from an MNO, not a subscriber identifier. However:
numbering.audit.v1replicates onlyvalueHashed = sha256(value)toanalytics-service(reduces blast radius of a downstream breach).- Operational events (
number.assigned.v1, etc.) include the rawvaluebecause downstream consumers (billing, sender-id-registry) need to match inventory to their own records.
tenantIdis always raw UUID.actorUserIdis raw UUID. User-level action detail does not cross service boundaries.- Event payloads signed by producer's RS256 key (platform signing) and verified opportunistically by critical consumers (billing, regulator-portal).
- Streams use NATS authentication with service-account JWTs; each consumer subject is ACL-scoped.
6. Outbox Pattern
All state changes write an event row to numbering.outbox in the same PostgreSQL transaction as the aggregate change. A relay process publishes to NATS with retry + ack. This guarantees:
- No event without a persisted state change.
- No unpublished state change outlasting relay retry (bounded to minutes).
- Events are replayable by walking the outbox table.
Outbox relay publishes in strict created_at order per numberId to preserve causal ordering for downstream consumers that care about lease/lifecycle sequencing.
7. Schema Evolution
- Additive fields with non-required defaults are non-breaking within the same
schemaVersion. - Breaking changes bump to
number.<topic>.v2. Subjects coexist during a ≥ 90-day deprecation window. - Enum values are additive. Consumers MUST treat unknown enum values as
UNKNOWNand not fail. - Per ADR-0004 §14,
regionIdis the only field that may be removed in v2 (contingent on multi-region strategy change), with six months' notice.
End of EVENT_SCHEMAS.md