Skip to content

PRD: Newsletter Subscribers

ModuleCustomerPRD IDPRD-SUB-001
StatusReady to devFEATSUB
EpicPlaneBANA-1536
Date2026-04-03Versionv1.1
Packages@nx/outreachURDSUB
SurfaceBO · Ops
OwnerPhát Nguyễn

TL;DR

Visitors opt into the newsletter by email (with topics and language) and leave in one click via a unique token - this lifecycle is live. The back office currently has a read-only subscriber list (5 columns: email, status, topics, language, subscribed date) - built. The statistics view (total, new-this-month, active, unsubscribed) is a forward spec: the numbers foundation is ready system-side, the Back Office screen is not yet built (🚧). Net result: a clean, unique mailing list with idempotent subscribe/re-subscribe.

1. Context & Problem

The public Overture site collects newsletter sign-ups, and our operations team needs to answer the basics - how many subscribers, how many joined this month, how many opted out. Sign-ups also have to be safe on repeat: submitting the same email twice must not create duplicates, and a previously opted-out address must be able to rejoin without manual cleanup.

This feature builds the subscriber lifecycle (subscribe, unsubscribe, re-subscribe - live), a read-only back-office list (built), and a read-only statistics view (not yet built - the numbers computation is ready system-side, the display screen is missing).

2. Goals & Non-Goals

Goals

  • Subscribe by email with optional topics and language; keep the email unique across subscribers.
  • Idempotent subscribe: an already-active email returns the existing subscriber; a deactivated one is reactivated.
  • One-click unsubscribe via a unique token link - no authentication needed.
  • A read-only subscriber list in the back office - view and filter, no editing.
  • An authenticated statistics view for the back office - total, new-this-month, active, unsubscribed (🚧 not built).

Non-Goals

  • Email / SMS campaign engine - Planned (see URD §7).
  • Creating / editing / deleting subscribers from the back office - the list is read-only by design; data only enters via the public form.
  • Customer segmentation and targeting.
  • Subscriber lifetime-value analytics.
  • Email delivery / transport (owned outside this module).

3. Success Metrics

MetricTarget / signal
List integrityZero duplicate emails among subscribers; every subscriber has a unique unsubscribe token
Subscribe idempotencyRepeated subscribe of the same email yields no new entries; deactivated entries reactivate cleanly
Unsubscribe reliabilityOne-click token link deactivates on first request; an invalid token is rejected as not-found
VisibilityThe back-office list is viewable and filterable (available now); the statistics view returns the 4 numbers on demand (once built)

4. Personas & Use Cases

PersonaGoal in this feature
Site VisitorJoin the newsletter by email; leave in one click
Operations team (NEXPANDO & VNPAY)View and filter the subscriber list; track list size, monthly growth, and opt-outs

Core flow: a visitor subscribes by email (topics + language) → a repeat or previously-opted-out email is handled idempotently → the visitor unsubscribes via a token link → the operations team views the subscriber list in the back office; once the statistics view ships, they also read the 4 growth numbers.

5. User Stories

  • As a site visitor, I want to subscribe with my email, topics, and language, so that I receive the newsletter.
  • As a site visitor, I want subscribing twice to be safe, so that I never create duplicate sign-ups or errors.
  • As a previously-unsubscribed visitor, I want to subscribe again and be reactivated, so that I can rejoin without help.
  • As a site visitor, I want to unsubscribe in one click via a token link, so that opting out is effortless.
  • As the operations team, I want to view and filter the subscriber list in the back office, so I know who is on the list.
  • As the operations team, I want a statistics view of the list, so I can track growth and opt-outs without counting by hand.

6. Functional Requirements

#RequirementStatusURD ref
FR-1Subscribe by email (public, no authentication) with optional language and topics✅ builtURD-SUB-001
FR-2Unique email across active subscribers; the unsubscribe token is generated unique and hard to guess✅ builtURD-SUB-002
FR-3Idempotent subscribe - an active email returns the existing subscriber; a deactivated one is reactivated✅ builtURD-SUB-004
FR-4Unsubscribe by token (public) deactivates the subscriber; an invalid token is rejected as not-found✅ builtURD-SUB-003
FR-5Read-only subscriber list in the back office - 5 columns, 4 filterable✅ builtURD-SUB-005
FR-6Authenticated statistics view - 4 numbers: total, new-this-month, active, unsubscribed🚧 not builtURD-SUB-005

FR-1 - Subscribe by email (public)

  • Fields: email (required, valid format), topics of interest (optional, multiple), preferred language (optional - Vietnamese / English).
  • No sign-in needed; returns the subscriber's identifier.
  • The subscribe timestamp and the unsubscribe token are issued at creation.

FR-2 - Unique email & token

  • Email is unique across active subscribers, enforced by a uniqueness rule - there are never two active records for the same email.
  • The unsubscribe token is generated unique and hard to guess (high entropy), but is not enforced unique at the storage layer.

FR-3 - Idempotent subscribe

  • Email already active → return the existing subscriber; no new record, no error.
  • Email previously deactivated → reactivate: reset the subscribe timestamp to now, clear the unsubscribe timestamp, set status back to active.
  • State transitions are deterministic - the same input always yields the same result.

FR-4 - Unsubscribe by token

  • One click on a token-bearing link, no authentication → the subscriber becomes unsubscribed, with the unsubscribe timestamp recorded.
  • An invalid token → rejected as not-found; no information revealed.
  • Unsubscribing an already-deactivated entry is idempotent - no error, no further change.

FR-5 - Back-office subscriber list (read-only)

The only screen this feature currently has in the Back Office.

ColumnFilterableDisplay
Email
StatusColored label: Active / Unsubscribed
Topics-A list of labels, one per topic
LanguageLabel: Vietnamese / English
Subscribed atDate-time
  • Read-only: no create button, no edit, no delete - subscribers only enter the list via the public form.
  • The screen requires a back-office sign-in.

FR-6 - Statistics view (🚧 not built)

Current state: the numbers computation is ready system-side; the Back Office display screen is missing.

Spec for when it is built:

  • 4 numbers: total subscribers · new this month (joined since the start of the current month) · active · unsubscribed.
  • Computed in a single aggregation pass over non-deleted entries (soft-delete-aware); deleted entries never count.
  • Requires a signed-in session; not additionally permission-gated - anyone with back-office access can read it.
  • Intended placement: a numbers strip directly above the subscriber list table (FR-5), so the list and its health read in one place.

Acceptance criteria

Verifiable now (built):

  1. Subscribing a new email → the subscriber appears in the back-office list with status Active and the correct subscribed date.
  2. Subscribing the same email a second time → the list still shows exactly one row for that email.
  3. Unsubscribing via the token link → the corresponding row's label switches to Unsubscribed.
  4. Re-subscribing an unsubscribed email → that row returns to Active with a fresh subscribed date; no second row appears.
  5. Opening an unsubscribe link with a wrong token → a not-found response.
  6. The back-office list shows all 5 columns; filtering by Email, Status, Language, Subscribed at returns correct results; the Topics column offers no filter.
  7. The list screen has no create / edit / delete controls.

Pending the screen (statistics 🚧):

  1. The statistics view shows the 4 numbers correctly; total = active + unsubscribed (over non-deleted entries).
  2. "New this month" counts only those who joined since 00:00 on the first day of the current month.
  3. Soft-deleted subscribers appear in none of the numbers.
  4. Accessing the statistics view unauthenticated → rejected.

7. Non-Functional Requirements

AreaRequirement
Data integrityEmail unique across active subscribers; subscribe never creates duplicates; all entries are soft-deletable
IdempotencyRepeated subscribe is safe (returns existing or reactivates); state transitions are deterministic
Scope & authzPublic subscribe/unsubscribe require no auth; the back-office list and statistics require a signed-in session; the statistics read is not additionally permission-gated
Performance / scaleStatistics computed in a single aggregation pass, not row-by-row counting
i18nLanguage captured per subscriber; user-facing labels are bilingual (EN/VI)

8. UX & Flows

Key screens: the public newsletter sign-up on the Overture site, and the back-office subscriber list (built). The statistics view is planned as a numbers strip above the list (🚧).

9. Data & Domain

ConceptRole
SubscriberThe mailing-list entry - email (unique), status (active / unsubscribed), language, topics, subscribe and unsubscribe timestamps, and a unique unsubscribe token
Statistics summaryA read-only view over subscribers: total, new-this-month, counts by status

Conceptual only - full data model and invariants in the Outreach domain model.

10. Dependencies & Assumptions

Depends on

  • Outreach capability - hosts the subscriber lifecycle and the statistics numbers.
  • Back-office - the surface that shows the list (available) and the statistics (not yet built).
  • Overture public site - the source of newsletter sign-ups.

Assumptions

  • The public site can call the unauthenticated subscribe/unsubscribe actions.
  • Back-office users authenticate to view the list and statistics.

11. Risks & Open Questions

Risk / questionMitigation / status
Duplicate sign-ups from repeated submitsSubscribe is idempotent; email is globally unique
Unsubscribe token guessing / replayToken is high-entropy and hard to guess; an invalid token is rejected; unsubscribe is idempotent on deactivated entries
Statistics screen not built - the operations team can't self-serve growth numbers yetForward spec FR-6 keeps the requirement; the numbers foundation is ready system-side, only the display screen remains
No campaign engine to act on the listOut of scope; documented as Planned in the URD
Statistics cost as the list growsSingle aggregation pass; revisit caching if the back office polls heavily

12. Release Plan & Launch Criteria

AspectPlan
PhaseP2 - Customer feature SUB in the URD feature catalog; FR-1..FR-5 built, FR-6 not
RolloutAll deployments; no feature flag
MigrationNone (new entries; no data backfill)
Launch criteriaAll "verifiable now" criteria (§6, items 1-7) pass; the statistics group (items 8-11) is accepted once the FR-6 screen ships
MonitoringSubscriber growth (new-this-month), opt-out rate, unsubscribe error rate

13. FAQ

What happens if the same email subscribes twice? Subscribe is idempotent - an already-active email returns the existing subscriber, and a previously deactivated one is reactivated. No duplicate entry is created.

Does unsubscribe require login? No - it's a public one-click link carrying a unique token. The back-office list and statistics, by contrast, require sign-in.

What can the back office see today? The read-only subscriber list: email, status, topics, language, subscribed date - filterable on 4 columns (all except topics). No create/edit/delete from there.

Does the statistics view exist yet? Not yet - it is a forward spec (🚧). The numbers computation is ready system-side; the Back Office screen will be built per FR-6: 4 numbers, planned directly above the list.

Are deleted subscribers counted? No - statistics aggregate over non-deleted (soft-delete-aware) entries only.

References

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.