Skip to main content

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

StreamSubjectsRetentionReplicasDedup window
NUMBERING_EVENTSnumber.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.v113 months hot32 min
NUMBERING_AUDITnumbering.audit.v113 months hot + 7 years cold (S3 object-lock) (regulatory per ATRA MoU)32 min
NUMBERING_LEASESnumber.lease.imported.v1, number.lease.batch.completed.v17 years (regulatory)32 min
NUMBERING_OPSnumber.conflict.detected.v1, number.pool.exhausted.v1, number.renewal.failed.v190 days32 min
NUMBERING_REGULATORnumbering.regulator.export.generated.v17 years3

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

SubjectProducerAction
compliance.tenant.suspended.v1compliance-engineInvoke UC-15 BulkRecallByTenant with reason: ABUSE, ticketId = complianceCaseId
billing.account.delinquent.v1billing-serviceTransition tenant leases to SUSPENDED; after 14-day grace → RECALLED
billing.account.paid.v1billing-serviceReinstate SUSPENDED leases where grace window has not elapsed
senderid.revoked.v1sender-id-registry-serviceRecall the alpha-ID lease (reason: ABUSE with ticket reference)
tenant.deleted.v1auth-serviceRelease all reservations + recall all leases (reason: PLATFORM_RECALL)
mno.contract.updated.v1regulator-portal-serviceUpdate LeaseContract.effectiveUntil or status
tenant.plan.changed.v1billing-serviceRefresh TenantPool quotas within 60 s
fraud.signal.v1 (kind = ASSIGNMENT_BURST)fraud-intel-serviceAdvisory — triggers per-tenant soft-rate-limit on Reserve

4. Consumer Contracts (by service)

ServiceSubscribes toAction
billing-servicenumber.assigned.v1, number.released.v1, number.renewed.v1, number.recalled.v1Lifecycle billing (start / stop / renew / proration)
sender-id-registry-servicenumber.assigned.v1 (type=ALPHA_ID), number.recalled.v1 (type=ALPHA_ID)Keep sender-ID registry in lock-step with inventory truth
sms-orchestratornumber.assigned.v1, number.recalled.v1, number.suspended.v1Cache invalidation for ValidateLease
analytics-servicenumbering.audit.v1, number.*.v1Long-term archival + pool-utilisation dashboards
notification-servicenumber.pool.exhausted.v1, number.renewal.failed.v1Alert platform ops + tenant portal notifications
fraud-intel-servicenumber.conflict.detected.v1, number.reserved.v1 (for burst detection)Reputation inputs
regulator-portal-servicenumbering.regulator.export.generated.v1ATRA 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.v1 replicates only valueHashed = sha256(value) to analytics-service (reduces blast radius of a downstream breach).
    • Operational events (number.assigned.v1, etc.) include the raw value because downstream consumers (billing, sender-id-registry) need to match inventory to their own records.
  • tenantId is always raw UUID.
  • actorUserId is 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 UNKNOWN and not fail.
  • Per ADR-0004 §14, regionId is the only field that may be removed in v2 (contingent on multi-region strategy change), with six months' notice.

End of EVENT_SCHEMAS.md