consent-ledger-service — Service Overview
Version: 1.0 Status: Draft Owner: Trust & Safety Last Updated: 2026-04-21 References: ADR-0004 §3, 07-epics-and-user-stories.md §6.12, 13-security-compliance-tenancy.md
1. Purpose
consent-ledger-service is the platform's authoritative ledger of subscriber consent and opt-out state for SMS messaging. It owns:
- The National DND Registry sync (where ATRA maintains a national do-not-disturb list).
- Per-tenant per-MSISDN opt-in / opt-out records.
- Inbound MO STOP-keyword processing — translating subscriber-typed STOP/UNSUBSCRIBE/STOPALL/QUIT into consent revocation, propagated to all relevant tenant scopes.
- A regulator-defensible consent audit log (≥ 7 years, append-only, hash-chained).
- A
CheckConsentgRPC API consumed bycompliance-engineandrouting-engineso no message reaches a recipient who has revoked consent.
Without this service, the platform would systematically violate Afghan and international consent law (TCPA-equivalent, GDPR Article 7) and would be unable to defend a regulator complaint.
2. Bounded Context
| Concern | In scope | Out of scope |
|---|---|---|
| Per-tenant consent records | ✅ | Recipient identity / KYC (owned by MNO) |
| National DND registry sync | ✅ | DND list authorship (owned by ATRA) |
| Inbound MO STOP-keyword detection | ✅ | MO transport (owned by smpp-connector / channel-router) |
| Cross-service consent propagation | ✅ | Per-message blocking (compliance-engine enforces) |
| Multi-language STOP keywords (Pashto/Dari/Arabic/English) | ✅ | Free-text natural-language opt-out (deferred — Phase 2) |
| Consent audit log (7 y, append-only, hash-chained) | ✅ | General platform audit (auth.audit_log) |
3. Key Responsibilities
- Maintain
consent.recordsper(tenantId, msisdn, scope)with stateOPT_IN | OPT_OUT | UNKNOWN | EXPIRED. - Daily-pull and merge the National DND Registry from ATRA (where available); cache and apply to all tenants.
- Consume inbound MO from
channel-router-service/smpp-connectorfor STOP-keyword matches; revoke consent and emitconsent.revoked.v1. - Expose
CheckConsent(tenantId, msisdn, scope) → { allowed, reason }gRPC; consumed bycompliance-engine(CONSENT rule type) androuting-engine(last-mile veto). - Expose
RecordConsent(...)REST for tenants to record explicit opt-in (e.g., from web-form double-opt-in). - Maintain an append-only, hash-chained audit log of every consent state change for ≥ 7 years (regulator evidence window).
4. Dependencies
| Direction | Dependency | Reason |
|---|---|---|
| Inbound | compliance-engine (gRPC CheckConsent) | Pre-routing consent check |
| Inbound | routing-engine (gRPC CheckConsent) | Last-mile consent veto |
| Inbound | sms-firewall-service (gRPC CheckConsent) | Inbound MT firewall consent check |
| Inbound | channel-router-service / smpp-connector (NATS sms.mo.inbound) | STOP-keyword detection |
| Inbound | Tenant REST RecordConsent, RevokeConsent | Explicit opt-in recording |
| Inbound | Citizen-facing portal (/consent/{msisdn}) | Self-service consent inspection |
| Outbound | NATS (consent.granted.v1, consent.revoked.v1, dnd.registry.synced.v1) | Downstream notifications |
| Outbound | Postgres (consent schema) | Records, audit log |
| Outbound | Redis (consent:state:{tenant}:{msisdn}) | Hot-cache for CheckConsent (P95 ≤ 5 ms) |
| Outbound | ATRA SFTP/API | National DND pull |
| Outbound | Object storage (cold archive, ≥ 7 y) | Audit log retention |
5. Runtime Topology
6. Key Design Decisions
- Fail-closed on
CheckConsentcache miss + DB unavailable — when the hot cache misses and Postgres is unreachable, return{ allowed: false, reason: "CONSENT_UNKNOWN" }. Better to falsely block one message than to systematically violate consent law. - Scope is first-class — consent is not boolean: it is per
(tenantId, msisdn, scope)where scope ∈{TRANSACTIONAL, MARKETING, OTP, EMERGENCY}. STOP at the marketing scope does not revoke OTP. - National DND overrides everything — the National DND list (when ATRA publishes one) is a hard-block at the platform level, applicable regardless of tenant opt-in claim. Exception: lane=P0 emergency (CBC-bridge has its own opt-out semantics).
- Multi-language STOP keywords — recognised opt-out keywords include
STOP,STOPALL,UNSUBSCRIBE,QUIT,END,CANCEL(English);بند,لغو,پایان(Dari);بنديدل,لغو,ودرول(Pashto);إلغاء,وقف,إيقاف(Arabic). Configurable; per-tenant overrides allowed only to add additional keywords, not to remove defaults. - Hash-chained audit log — every consent transition row in
consent.auditcarriesprev_hash,payload_hash,record_hash. Regulator can verify integrity end-to-end. - Cold archive at 13 months → 7 y — hot Postgres holds 13 months; older rolled to S3 in monthly partitions. Restoration for regulator queries via async job (≤ 1 h).
7. Surface Inventory
| Interface | Purpose | Auth |
|---|---|---|
gRPC CheckConsent(tenantId, msisdn, scope) → { allowed, reason, cachedAt } | Pre-routing consent check; P95 ≤ 5 ms | Service mesh mTLS |
gRPC RecordConsent(tenantId, msisdn, scope, source) | Tenant opt-in submission | Service mesh mTLS + tenant scope |
gRPC RevokeConsent(tenantId, msisdn, scope, reason) | Explicit revocation | Service mesh mTLS + tenant scope |
REST POST /v1/consent/records | Tenant API: record opt-in | Kong JWT |
REST DELETE /v1/consent/records/:msisdn | Tenant API: revoke | Kong JWT |
REST GET /v1/consent/audit?msisdn= | Citizen self-service: see who has my consent and when granted | Kong JWT (citizen) or admin role |
REST GET /v1/admin/consent/dnd-registry | DND registry inspection | Admin role |
HTTP /health/live, /health/ready, /metrics | K8s + Prom | None / cluster |
NATS produce consent.granted.v1, consent.revoked.v1, dnd.registry.synced.v1 | Downstream | — |
NATS consume sms.mo.inbound | STOP-keyword detection | — |
8. Data Ownership
consent schema:
consent.records—(tenantId, msisdn, scope) PK, state, sourceEvent, validUntilconsent.audit— append-only, hash-chainedconsent.dnd_registry— National DND mirror (per-MSISDN flags, lastSyncAt)consent.stop_keywords— per-language opt-out keyword catalogconsent.tenant_scope_config— tenant-defined additional scopes (extensions)
Redis: consent:state:{tenantId}:{msisdn}:{scope} (TTL 300 s).
9. Failure Modes
- ATRA DND sync stale > 24 h → alert; degraded mode (use last-known DND).
- STOP-keyword consumer lag > 60 s → alert; subscriber's STOP not honoured promptly is a regulator risk.
CheckConsentcache + DB both unavailable → fail-closed (returnallowed=false); P0 emergency lane bypass with audit row.- Hash-chain break → critical alert; investigation; no consent records discarded.
10. Open Points
| ID | Question | Owner | Resolution |
|---|---|---|---|
| CONS-OPEN-001 | National DND registry — does ATRA publish one today? | Regulator Liaison | TBD |
| CONS-OPEN-002 | Scope catalog — final taxonomy beyond TRANSACTIONAL/MARKETING/OTP/EMERGENCY? | T&S Council | TBD |
| CONS-OPEN-003 | Citizen-portal authentication — MNO-confirmed phone-number verification or alternative? | Product + MNO | TBD |
| CONS-OPEN-004 | Cross-tenant STOP — STOP from MSISDN to one tenant should it propagate to all tenants? Defaults to per-tenant; configurable. | Legal + Compliance | Tentative: per-tenant default |