Skip to content

SMS opt-out list is a local JSON file loaded into memory: replicas disagree on who opted out #1130

Description

@nanaf6203-bit

SMS opt-out list is a local JSON file loaded into memory: replicas disagree on who opted out

Labels / Complexity: bug · Medium Complexity — Medium

Problem

SmsService (src/notifications/sms.service.ts) keeps opt-outs in an in-memory Set (optedOutPhones) that is loaded from and persisted to a single local JSON file via loadOptOutList() (line 344) and persistOptOutList() (line 355) at this.optOutStoragePath.

  • Replicas each hold their own opt-out list. The file is on local disk; with horizontal scaling, a phone opted out on pod A is not in pod B's set, so pod B keeps sending SMS to an opted-out number — a regulatory/compliance failure for a messaging product.
  • Concurrent writes corrupt or lose entries. persistOptOutList does a read-modify-write of the whole JSON file; two opt-outs racing (or an opt-out racing a send) can silently drop an entry.
  • Redeploys can wipe the list. If the file path is not on a persistent volume, a new pod starts with an empty set and sends to opted-out numbers until the list is re-imported.

Why this is architecturally hard

  1. Where opt-outs belong is a real decision. The durable home is a DB table (the module already has PrismaService-adjacent infrastructure in NotificationsModule) or Redis (the CacheService in src/cache/); the choice determines read cost on every send.
  2. The send path must stay fast. Opt-out checks run per message; the fix must keep the check cheap (cached set with a durable source of truth) rather than querying on every send.
  3. Migration must not lose existing opt-outs. Existing file contents need to be imported once, which is a one-time migration step the design must include.

Acceptance criteria

  • An opt-out recorded on any replica is enforced on all replicas.
  • Concurrent opt-outs do not lose entries.
  • The opt-out list survives a redeploy without requiring a persistent volume hack.
  • Tests cover cross-instance enforcement, concurrent opt-outs, and restart persistence.

Out of scope

The SMS rate limiter (#55); messaging content.

Getting started

  • src/notifications/sms.service.ts:344-370 — the load/persist pair and the set
  • src/cache/cache.service.ts — shared-state primitives

Commands: npm test.

Good first files to read: src/notifications/sms.service.ts (opt-out methods), src/cache/cache.service.ts.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions