Skip to main content

sender-id-registry-service — Domain Model

Version: 1.0 Status: Draft Owner: Trust & Safety + Regulator-facing Last Updated: 2026-04-21 Companion: SERVICE_OVERVIEW · APPLICATION_LOGIC · DATA_MODEL · EVENT_SCHEMAS · API_CONTRACTS · SECURITY_MODEL


1. Bounded Context

National Sender-ID Authority. This service owns the canonical record of every alphanumeric sender-ID, short-code, and long-code authorised on the Ghasi national SMS backbone, including KYC of the registrant, verification artefacts, lifecycle state, reputation, and the regulator-facing export to ATRA. It is a peer to compliance-engine, routing-engine, and sms-firewall-service; none of those services maintain their own copy of sender-ID truth — they consume Verify(senderId, tenantId) over gRPC.

The benchmark precedents this service mirrors:

  • India DLT (Distributed Ledger Telecom) — Principal Entity registration + Header registration + Template registration enforced via mandatory pre-clearance.
  • KSA CITC Sender-ID Registry — bank/government category enforcement and notarised verification.
  • US 10DLC Brand & Campaign Registry (TCR) — brand vetting + use-case categorisation + reputation throttling.

The context boundary is drawn such that:

  • Inside the boundary: sender-IDs (alpha/short/long), KYC documents (S3 references + hashes), verification challenges & artefacts, restricted-name pattern catalogue, registry state machine, reputation snapshots, regulator export records, audit log.
  • Outside the boundary: message storage (sms-orchestrator), per-message rule evaluation (compliance-engine), routing decisions (routing-engine), tenant identity (auth-service), MSISDN inventory & leasing (numbering-service), fraud detection itself (fraud-intel-service produces signals that feed reputation; this service only consumes them), DLR processing (dlr-processor), tenant compliance score (compliance-engine owns it).

Per ADR-0004 §3, sender-id-registry-service is one of the twelve new bounded contexts introduced for national-backbone GA. Per ADR-0004 §5, sender-ID data is multi-master across kbl and mzr with conflict-free updates (logical replication + per-row LWW with HLC) — this is control-plane data, not data-plane traffic.


2. Aggregates

SenderId

The root aggregate. One row per registered sender-ID across the national platform. Once REVOKED, the value is reserved for 12 months (US-SID-014) and any re-registration must traverse the full registration + verification cycle.

FieldTypeNotes
senderIdInternalIdUUIDv4Internal identity (stable across renames)
valueSenderIdValue VONormalised display form (e.g. BANK-XYZ, 7000, +93701234567)
typeenum SenderIdTypeALPHA · SHORT · LONG
categoryenum SenderIdCategoryBANKING · GOVERNMENT · HEALTHCARE · UTILITIES · MNO_INTERNAL · RETAIL · TRANSPORT · EDUCATION · OTHER
tenantIdUUIDv4Owning tenant (the registrant tenant, not the brand)
registrantOrgNamestringLegal name of the registering organisation, as filed in commercial registry
registrantContactEmailstringVerified email of registrant compliance officer
registrantContactMsisdnE.164Used for OTP verification
restrictedPatternIdUUIDv4 | nullSet when value matched a RestrictedPattern at submission time
requiredVerificationLevelVerificationLevelMinimum level required for ACTIVE state (derived from category + restricted-pattern match)
currentVerificationLevelVerificationLevelCurrently achieved level (see §3)
stateRegistryStateSee §4 — state machine
reservedUntilInstant | nullFor REVOKED rows, the date the value becomes available again (12 months)
firstSubmittedAt, kycApprovedAt, verifiedAt, activatedAt, suspendedAt, revokedAt, lastReviewedAtInstant | nullLifecycle timestamps
lastSuspendReason, lastRevokeReasonstring | nullMandatory on transition
versionintOptimistic-lock counter
createdAt, updatedAtInstant

Invariants

  • value + type is globally unique across all state ∈ {SUBMITTED, KYC_REVIEW, KYC_APPROVED, VERIFIED, ACTIVE, SUSPENDED} rows. REVOKED rows reserve the value until reservedUntil.
  • type = ALPHA: value matches ^[A-Za-z0-9]{1,11}$ (per 3GPP TS 23.038, GSM-7 alphanumeric, 11-char alpha-ID limit). Lowercase normalised to uppercase for uniqueness checks.
  • type = SHORT: value matches ^[0-9]{4,6}$.
  • type = LONG: value matches E.164 (^\+[1-9][0-9]{6,14}$).
  • restrictedPatternId IS NOT NULLrequiredVerificationLevel = NOTARISED AND a regulator-letter document is present in KycDocument set (US-SID-005).
  • Transition into ACTIVE requires currentVerificationLevel ≥ requiredVerificationLevel (ordering in §3).
  • state = REVOKED is terminal; no transitions out. Re-registration creates a new SenderId row only after now() ≥ reservedUntil.
  • tenantId is immutable after KYC_APPROVED. Transfers of brand ownership require REVOKE + new registration.

KycDocument

A KYC artefact attached to a SenderId. The actual file lives in object storage (per-tenant encrypted bucket); this aggregate stores the pointer, hash, classification, and audit trail of who has viewed it.

FieldTypeNotes
kycDocIdUUIDv4Identity
senderIdInternalIdUUIDv4Parent
docTypeenumCOMMERCIAL_LICENCE · NATIONAL_ID · REGULATOR_LETTER · NOTARISED_AUTHORITY · BOARD_RESOLUTION · DOMAIN_OWNERSHIP_PROOF · OTHER
s3Refstrings3://ghasi-sid-kyc-{region}/{tenantId}/{kycDocId}.{ext}
mimeTypestringapplication/pdf · image/jpeg · image/png · image/heic
sizeBytesint≤ 25 MB enforced at upload
sha256HexstringSHA-256 of the encrypted blob; immutable post-upload
encryptionKeyIdstringVault Transit key reference (per-tenant KEK)
uploadedByUUIDv4User who uploaded
uploadedAtInstant
verificationOutcomeenum | nullACCEPTED · REJECTED · PENDING
verificationNotesstring | nullReviewer notes on this specific document

Invariants

  • sha256Hex is computed at upload time and never recomputed; tamper detection runs nightly comparing stored hash to S3 ETag-extended attribute.
  • A SenderId with restrictedPatternId IS NOT NULL MUST have ≥ 1 KycDocument of docType = REGULATOR_LETTER and ≥ 1 of docType = NOTARISED_AUTHORITY.
  • KYC documents are never deleted — on tenant erasure, the row is tombstoned and the S3 blob is overwritten with a sealed redaction marker (per GDPR Art. 17 reconciled with regulatory evidence retention).

Verification

A verification challenge + outcome record. One SenderId may accumulate many Verification rows over its lifetime (multiple attempts, level upgrades, periodic re-verification).

FieldTypeNotes
verificationIdUUIDv4Identity
senderIdInternalIdUUIDv4Parent
methodenum VerificationMethodOTP · DOCUMENT · NOTARISED · DOMAIN_DNS
challengeJSONBMethod-specific (DNS TXT value to publish, OTP hash, doc reference)
attemptsintCount of validation attempts on this challenge
maxAttemptsintMethod-specific cap (OTP=3, DNS=5/24h, NOTARISED=N/A)
stateenumPENDING · IN_PROGRESS · SUCCEEDED · FAILED · EXPIRED
expiresAtInstantOTP=5 min; DNS=24 h window; DOCUMENT/NOTARISED=14 d reviewer SLA
levelOnSuccessVerificationLevelLevel granted on SUCCEEDED
attemptedByUUIDv4Initiating user
succeededAt, failedAt, failureReason

Invariants

  • attempts ≤ maxAttempts. On overflow, state becomes FAILED with failureReason = max_attempts_exceeded.
  • OTP challenges store only the SHA-256 hash of the code; the plaintext OTP exists for at most 5 min in Redis.
  • DOMAIN_DNS challenge value is sha256(senderIdInternalId || nonce) truncated to 32 chars, expected as _ghasi-sid-verify.{domain} TXT record.
  • Successful Verification writes update SenderId.currentVerificationLevel to MAX(prev, levelOnSuccess) (level monotone-increasing within a registration).

RestrictedPattern

The platform's catalogue of regex patterns matching impersonation-prone names. Maintained by Trust & Safety + Legal; admin-only CRUD.

FieldTypeNotes
patternIdUUIDv4Identity
patternstringCompiled with re2; ReDoS-screened
categoryenumBANK · GOV · MNO · JUDICIAL · HEALTH · EMERGENCY · OTHER_RESERVED
requiredVerificationLevelVerificationLevelMinimum (typically NOTARISED)
requiredDocTypesenum[]E.g. [REGULATOR_LETTER, NOTARISED_AUTHORITY]
regulatorRefstring | nullATRA circular / law reference justifying restriction
isActivebooleanSoft-disable
notesstringReviewer-facing rationale
createdBy, updatedBy, createdAt, updatedAt, versionAudit

Invariants

  • A submission matching ≥ 1 active pattern is forced to the strictest matching pattern's requirements (max of required levels, union of required doc types).
  • Patterns are never deleted — soft-disable only — so historical match decisions remain explainable.
  • Default seed patterns (US-SID-005): ^BANK[A-Z0-9]*$, ^GOV[A-Z0-9]*$, ^MOJ[A-Z0-9]*$, ^AWCC[A-Z0-9]*$, ^ROSHAN[A-Z0-9]*$, ^ETISALAT[A-Z0-9]*$, ^MTN[A-Z0-9]*$, ^SALAAM[A-Z0-9]*$, ^DAB[A-Z0-9]*$ (Da Afghanistan Bank), ^MOPH[A-Z0-9]*$ (Ministry of Public Health), ^ATRA[A-Z0-9]*$ (regulator itself), ^EMERG[A-Z0-9]*$, ^POLICE[A-Z0-9]*$.

ReputationSnapshot

Append-only daily snapshot of a sender-ID's reputation score. The most recent row is the authoritative current score; history is preserved for trend analysis (US-SID-019) and for explaining auto-suspension decisions.

FieldTypeNotes
senderIdInternalIdUUIDv4
snapshotAtInstantDaily 00:30 UTC + intra-day delta updates
scoreint 0..100Per ReputationScore VO
complianceHits7d, complaints7d, fraudHits7d, deliveryRate7dInputs (cached for UI explainability)
deltaintChange from previous snapshot
triggerenumDAILY_CRON · FRAUD_EVENT · COMPLIANCE_BLOCK · MANUAL_RESET
triggerEventIdUUIDv4 | nullFor non-cron triggers

Invariants

  • Append-only — UPDATE/DELETE rejected at the DB level (see DATA_MODEL §3.1).
  • score is clamped to [0, 100]. The cron computation may produce out-of-range arithmetic; the persistence layer clamps before INSERT.
  • A sender-ID without ≥ 1 snapshot defaults to neutral 50 in Verify responses (no signal yet).

RegulatorExport

A record of each export of the registry to ATRA (US-SID-016). Maintained for regulator-side reconciliation and for our own audit of what ATRA has been told.

FieldTypeNotes
exportIdUUIDv4Identity
exportedAtInstantWhen the file was generated
windowFrom, windowToInstantExport coverage
formatenumJSONL · CSV · XML_ATRA_v1
s3Refstrings3://ghasi-sid-regulator-export-{region}/{exportId}.{ext}
s3RefSignaturestringDetached signature using regulator-export key (HSM-held)
rowCountintSender-IDs included
transmittedTostring | nullSFTP host or API endpoint receipt
transmittedAtInstant | null
acknowledgmentRefstring | nullATRA-side receipt id
triggeredByenumCRON · ON_DEMAND · REGULATOR_REQUEST

Invariants

  • The export file is immutable post-creation; corrections require a new RegulatorExport row with format = XML_ATRA_v1 adjustment record (analogous to CDR adjustment per ADR-0004 §15).
  • KYC document content is never included in any export (per US-SID-016) — only registry metadata.

AuditEntry

Append-only audit row for every state change, KYC document view, restricted-pattern change, and regulator export. Long retention (≥ 13 months hot, ≥ 7 years cold per ADR-0004 §9).

FieldTypeNotes
auditIdUUIDv4
entityTypeenumSENDER_ID · KYC_DOC · KYC_DOC_VIEW · VERIFICATION · RESTRICTED_PATTERN · REGULATOR_EXPORT · REPUTATION
entityIdUUIDv4
actionenumCREATE · UPDATE · APPROVE · REJECT · REQUEST_INFO · SUSPEND · REACTIVATE · REVOKE · VIEW · EXPORT · RESET
actorUserIdUUIDv4
actorRolestringCaptured at the moment of the action
before, afterJSONBSnapshot
reasonstringMandatory on suspend/revoke/reject
ip, userAgent, traceIdForensic
occurredAtInstant

Invariants

  • Append-only at the database level (Postgres rules reject UPDATE/DELETE — see DATA_MODEL §3.1).
  • Every SUSPEND, REVOKE, REJECT, REQUEST_INFO action requires a non-empty reason; enforced at the API boundary and re-checked in the persistence layer.
  • KYC document views write entityType = KYC_DOC_VIEW so unauthorised reading patterns are detectable.

3. Value Objects

VOShapeInvariants
SenderIdValuestring normalisedTrim; for ALPHA upper-case for uniqueness; for SHORT strip non-digits; for LONG enforce E.164
SenderIdTypeALPHA | SHORT | LONG
VerificationLevelNONE | OTP | DOCUMENT | NOTARISED | DOMAIN_DNSOrdering: NONE < OTP < DOCUMENT < NOTARISED. DOMAIN_DNS is orthogonal — a sender-ID can hold both NOTARISED and DOMAIN_DNS simultaneously. The combined level (NOTARISED + DOMAIN_DNS) is required by P0/P1 lanes for restricted categories.
RegistryStateSUBMITTED | KYC_REVIEW | KYC_APPROVED | KYC_REJECTED | INFO_REQUESTED | VERIFIED | ACTIVE | SUSPENDED | REVOKEDSee §4
ReputationScoreint 0..100Clamp; < 30 ⇒ AUTO_SUSPEND; 30..49 ⇒ POOR; 50..69 ⇒ NEUTRAL; 70..89 ⇒ GOOD; ≥ 90 ⇒ EXCELLENT
VerifyContext{ senderIdValue, senderIdType, tenantId }Used by Verify gRPC; tenantId cross-checked against SenderId.tenantId

4. Registry State Machine

┌───────────┐
│ SUBMITTED │ ← POST /v1/sender-ids
└─────┬─────┘
│ assignReviewer / KYC complete

┌───────────────┐
│ KYC_REVIEW │
└──┬─────┬──┬───┘
request_info│ reject│ approve
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌──────────────┐ ┌──────────────┐
│ INFO_REQUESTED │ │ KYC_REJECTED │ │ KYC_APPROVED │
└────────┬────────┘ └──────────────┘ └──────┬───────┘
│ resubmit │ first verification succeeds
└──────────────────────► KYC_REVIEW │

┌─────────────┐
│ VERIFIED │
└──────┬──────┘
│ admin activate

┌─────────────┐
┌────────────────────────────│ ACTIVE │◄───┐
│ reactivate └──────┬──────┘ │
│ │ suspend │
▼ ▼ │
┌─────────────┐ ┌─────────────┐ │
│ REVOKED │ ◄───── revoke ──────│ SUSPENDED │────┘
└─────────────┘ └─────────────┘
(terminal, (auto on rep<30
12-mo reserved) or manual)

Allowed transitions (enforced as a database CHECK + service-layer guard):

FromToTriggerAuthoriser
(n/a)SUBMITTEDTenant submitsTenant
SUBMITTEDKYC_REVIEWReviewer claimsT&S reviewer
KYC_REVIEWKYC_APPROVEDReviewer approvesT&S reviewer
KYC_REVIEWKYC_REJECTEDReviewer rejectsT&S reviewer
KYC_REVIEWINFO_REQUESTEDReviewer requests infoT&S reviewer
INFO_REQUESTEDKYC_REVIEWTenant resubmitsTenant
KYC_APPROVEDVERIFIEDFirst successful verification ≥ required levelsystem
VERIFIEDACTIVEAdmin activateT&S admin
ACTIVESUSPENDEDManual or auto (rep < 30)T&S admin / system
SUSPENDEDACTIVEReactivate (with remediation evidence)T&S admin
SUSPENDEDREVOKEDRevoke for causeT&S admin
ACTIVEREVOKEDRevoke for causeT&S admin
REVOKED(none)terminal
KYC_REJECTED(none)terminal — re-registration creates a new row

5. Domain Events (produced)

Detailed schemas in EVENT_SCHEMAS.md. Stream SENDER_ID_EVENTS (mirrored across kblmzr per ADR-0004 §13).

EventTrigger
sender.id.submitted.v1Tenant submits via POST /v1/sender-ids
sender.id.kyc_approved.v1Reviewer approves KYC
sender.id.kyc_rejected.v1Reviewer rejects
sender.id.info_requested.v1Reviewer requests more info
sender.id.verified.v1A successful Verification raises currentVerificationLevel
sender.id.activated.v1Admin transitions to ACTIVE
sender.id.suspended.v1Manual or auto suspension
sender.id.reactivated.v1Reactivation from SUSPENDED
sender.id.revoked.v1Revoke transition
sender.id.reputation.changed.v1Reputation crosses 30/50/70/90 boundary
sender.id.regulator.exported.v1Regulator export completes

6. Reputation Scoring Formula

The daily cron (US-SID-017, see APPLICATION_LOGIC §1 UC-12) computes:

score = 100
− (complianceHits_7d × 2)
− (complaints_7d × 5)
− (fraudHits_7d × 10)
− ((1 - deliveryRate_7d) × 30)
score = clamp(score, 0, 100)

Inputs:

  • complianceHits_7d — count of compliance.message.blocked.v1 and compliance.message.held.v1 events whose senderId matches, in the trailing 7 days.
  • complaints_7d — count of citizen complaints from regulator-portal-service complaint ingest in the trailing 7 days.
  • fraudHits_7d — weighted count of fraud.detected.* events (AIT=1, SIM-box=2, OTP harvesting=3) in the trailing 7 days.
  • deliveryRate_7d — DLR success ratio from dlr-processor aggregates in the trailing 7 days.

Threshold actions:

  • score < 30 ⇒ auto-suspend (US-SID-012). Tenant notified; T&S can override.
  • score ∈ [30, 49] ⇒ POOR — eligible for restricted-lane only (P3/P4); not allowed on P1 OTP lane.
  • score ∈ [50, 69] ⇒ NEUTRAL — default after first successful registration.
  • score ∈ [70, 89] ⇒ GOOD.
  • score ≥ 90 ⇒ EXCELLENT — eligible for trusted-tenant fast path (per ADR-0004 §10).

7. Global Invariants

  • Public-but-private. The fact of registration (value, registrant org name, verification level, status) is public. KYC documents are never public (US-SID-015).
  • Audit-immortal. Sender-IDs are never hard-deleted. Every state change is captured in AuditEntry with before/after JSON.
  • Restricted-name protection. A submission whose normalised value matches an active RestrictedPattern cannot reach ACTIVE without NOTARISED AND a regulator-letter document. This invariant is enforced at submission (refuses incomplete submissions) and re-checked at activation.
  • Verification monotone. currentVerificationLevel never decreases through a successful Verification. Suspension/revocation does not lower the level — it gates use, not the achieved level.
  • Tenant isolation on KYC. A tenant can only fetch KycDocument belonging to a SenderId they own. T&S reviewer access is logged as KYC_DOC_VIEW.
  • Verify is the source of truth. Consumers (compliance-engine, routing-engine, sms-firewall-service) never read the registry tables directly. They call Verify over gRPC and respect cache invalidation events.
  • Fail-closed on Verify. If Verify cannot return a verdict (DB down, cache cold-miss + DB timeout), the consumer treats it as status: UNKNOWN and applies the fail-closed posture for its lane (compliance HOLDs the message; routing rejects with sender_id_unknown).
  • 12-month name reservation on revoke. value reserved by reservedUntil = revokedAt + 365d to prevent drive-by re-registration of revoked impersonator names.