PRD: Newsletter Subscribers
| Module | Customer (CORE-09) | PRD ID | PRD-SUB-001 |
| Status | Shipped | Owner | Customer squad |
| Date | 2026-04-03 | Version | v1.0 |
| Packages | @nx/outreach · @nx/core · apps/bo | URD | SUB |
TL;DR
Lets a site visitor opt into the newsletter by email, with topics and locale, then unsubscribe in one click via a unique token - and gives the back-office a single, authenticated statistics view of how the list is growing (total, new-this-month, counts by status). The result: a clean, globally-unique mailing list with idempotent subscribe/re-subscribe and a health dashboard, instead of an opaque sign-up form with no admin visibility.
1. Context & Problem
The public Overture marketing site collects newsletter sign-ups, but the back-office (apps/bo) has no way to read the list or see how it is growing. A visitor can subscribe, yet an admin cannot answer basic questions - how many subscribers exist, how many joined this month, how many have opted out. Sign-ups also need to behave safely under repeats: the same email submitted twice must not create duplicates, and a previously opted-out address must be able to rejoin without manual cleanup. Without a unique-email guarantee, a one-click unsubscribe, and an authenticated statistics read, the list is opaque and error-prone - a blocker for the marketing-engagement layer Customer targets.
This feature builds the subscriber lifecycle (subscribe, unsubscribe, re-subscribe) on the @nx/outreach service and exposes a read-only statistics aggregate to the back-office.
2. Goals & Non-Goals
Goals
- Subscribe by email with optional topics and locale, keeping the email globally unique among subscribers.
- Idempotent subscribe: an already-active email returns the existing subscriber; a deactivated one is reactivated.
- One-click unsubscribe via a unique token link, with no authentication required.
- An authenticated statistics endpoint for the back-office - total, monthly-new, and counts grouped by activated/deactivated status.
Non-Goals
- Email / SMS campaign engine - Planned (see URD §7).
- Customer segmentation and targeting.
- Subscriber lifetime-value analytics.
- Email delivery / SMTP transport (owned outside this module).
3. Success Metrics
| Metric | Target / signal |
|---|---|
| List integrity | Zero duplicate emails among subscribers; every subscriber has a unique unsubscribe token |
| Subscribe idempotency | Repeated subscribe of the same email yields no new rows; deactivated rows reactivate cleanly |
| Unsubscribe reliability | One-click token link deactivates on first request; invalid token returns 404 |
| Admin visibility | Back-office statistics view returns total, monthly-new, and per-status counts on demand |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Site Visitor | Join the newsletter by email; leave in one click |
| Marketing | See list size, monthly growth, and opt-out counts in the back-office |
| Owner | Confirm the mailing list is healthy and growing |
Core scenarios: a visitor subscribes by email (topics + locale) → a repeat or previously-opted-out email is handled idempotently → the visitor unsubscribes via a token link → marketing opens the back-office and reads the subscriber statistics.
5. User Stories
- As a site visitor, I want to subscribe with my email, topics, and locale, 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 marketing, I want an authenticated statistics view of the list, so that I can track growth and opt-outs.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Subscribe by email (POST /subscribe, no auth) with optional locale (vi/en) and optional topics[]; returns the subscriber id | URD-SUB-001 |
| FR-2 | Email is globally unique among subscribers; unsubscribe token is globally unique | URD-SUB-002 |
| FR-3 | Subscribe is idempotent - an already-active email returns the existing subscriber; a deactivated one is reactivated (subscribedAt reset, unsubscribedAt cleared) | URD-SUB-004 |
| FR-4 | Unsubscribe by token (GET /unsubscribe?token=…, no auth) deactivates the subscriber; an invalid token returns 404 | URD-SUB-003 |
| FR-5 | Statistics endpoint (GET /statistics, JWT or Basic auth) returns total, monthlyNew, and byStatus { activated, deactivated } | URD-SUB-005 |
| FR-6 | Statistics are a single SQL aggregate over non-deleted rows: COUNT(*) FILTER (…) for monthly-new (subscribedAt >= startOfMonth) and per-status counts | URD-SUB-005 |
Full requirement text and acceptance criteria live in the Customer URD. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | Email and unsubscribe token are globally unique; subscribe never creates duplicates; all rows use soft-delete |
| Idempotency | Repeated subscribe is safe (returns existing or reactivates); state transitions are deterministic |
| Tenancy & authz | Public subscribe/unsubscribe require no auth; statistics requires JWT or BASIC strategy; permissions declared via crudPermissions(Subscriber.AUTHORIZATION_SUBJECT, …) |
| Performance / scale | Statistics computed in a single SQL aggregate (COUNT(*) FILTER), not row-by-row counting |
| i18n | Locale captured per subscriber (vi/en); 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 + statistics view in apps/bo.
9. Data & Domain
| Entity | Role |
|---|---|
Subscriber | The mailing-list entry - email (unique), status (activated/deactivated), locale, topics[], subscribedAt, unsubscribedAt, unique unsubscribeToken |
| Statistics aggregate | A read-only projection over Subscriber: total, monthlyNew, and byStatus counts |
Conceptual only - full schema and invariants in the Outreach domain model.
10. Dependencies & Assumptions
Depends on
@nx/outreach- hosts the subscriber service, repository, and routes.@nx/core- provides theSubscribermodel, schema, andcrudPermissions.apps/bo- the back-office surface that reads the statistics endpoint.- Overture public site - the source of newsletter sign-ups.
Assumptions
- The public site can call the unauthenticated subscribe/unsubscribe routes.
- Back-office users authenticate via
JWTorBASICto read statistics.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Duplicate sign-ups from repeated submits | Subscribe is idempotent; email is globally unique |
| Unsubscribe token guessing / replay | Token is globally unique; invalid token returns 404; unsubscribe is idempotent on deactivated rows |
| No campaign engine to act on the list | Out of scope; documented as Planned in the URD |
| Statistics cost as the list grows | Single SQL aggregate; revisit caching if the back-office polls heavily |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 - Customer feature SUB in the URD feature catalog |
| Rollout | All deployments; no feature flag |
| Migration | None (new entity; no data backfill) |
| Launch criteria | Subscribe/unsubscribe/re-subscribe verified idempotent end-to-end; statistics endpoint returns correct total, monthly-new, and per-status counts; back-office list screen renders the data |
| Monitoring | Subscriber growth (monthly-new), opt-out rate (deactivated count), 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 row is created.
Does unsubscribe require login? No - unsubscribe is a public one-click link carrying a unique token. Statistics, by contrast, requires JWT or Basic auth.
What does the statistics endpoint return? total subscribers, monthlyNew (joined since the start of the current month), and byStatus counts split into activated and deactivated.
Does this feature send emails? No - it manages the subscriber list and its statistics. The campaign/delivery engine is Planned and lives outside this module.
Are deleted subscribers counted? No - statistics aggregate over non-deleted (soft-delete-aware) rows only.
References
- URD: Customer - Newsletter Subscribers
- Module: Customer - overview + traceability
- Developer: @nx/outreach · Outreach domain model