PRD: Newsletter Subscribers
| Module | Customer | PRD ID | PRD-SUB-001 |
| Status | Ready to dev | FEAT | SUB |
| Epic | — | Plane | BANA-1536 |
| Date | 2026-04-03 | Version | v1.1 |
| Packages | @nx/outreach | URD | SUB |
| Surface | BO · Ops | ||
| Owner | Phá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
| 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 entries; deactivated entries reactivate cleanly |
| Unsubscribe reliability | One-click token link deactivates on first request; an invalid token is rejected as not-found |
| Visibility | The back-office list is viewable and filterable (available now); the statistics view returns the 4 numbers on demand (once built) |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Site Visitor | Join 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
| # | Requirement | Status | URD ref |
|---|---|---|---|
| FR-1 | Subscribe by email (public, no authentication) with optional language and topics | ✅ built | URD-SUB-001 |
| FR-2 | Unique email across active subscribers; the unsubscribe token is generated unique and hard to guess | ✅ built | URD-SUB-002 |
| FR-3 | Idempotent subscribe - an active email returns the existing subscriber; a deactivated one is reactivated | ✅ built | URD-SUB-004 |
| FR-4 | Unsubscribe by token (public) deactivates the subscriber; an invalid token is rejected as not-found | ✅ built | URD-SUB-003 |
| FR-5 | Read-only subscriber list in the back office - 5 columns, 4 filterable | ✅ built | URD-SUB-005 |
| FR-6 | Authenticated statistics view - 4 numbers: total, new-this-month, active, unsubscribed | 🚧 not built | URD-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.
| Column | Filterable | Display |
|---|---|---|
| ✔ | ||
| Status | ✔ | Colored label: Active / Unsubscribed |
| Topics | - | A list of labels, one per topic |
| Language | ✔ | Label: Vietnamese / English |
| Subscribed at | ✔ | Date-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):
- Subscribing a new email → the subscriber appears in the back-office list with status Active and the correct subscribed date.
- Subscribing the same email a second time → the list still shows exactly one row for that email.
- Unsubscribing via the token link → the corresponding row's label switches to Unsubscribed.
- Re-subscribing an unsubscribed email → that row returns to Active with a fresh subscribed date; no second row appears.
- Opening an unsubscribe link with a wrong token → a not-found response.
- The back-office list shows all 5 columns; filtering by Email, Status, Language, Subscribed at returns correct results; the Topics column offers no filter.
- The list screen has no create / edit / delete controls.
Pending the screen (statistics 🚧):
- The statistics view shows the 4 numbers correctly; total = active + unsubscribed (over non-deleted entries).
- "New this month" counts only those who joined since 00:00 on the first day of the current month.
- Soft-deleted subscribers appear in none of the numbers.
- Accessing the statistics view unauthenticated → rejected.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | Email unique across active subscribers; subscribe never creates duplicates; all entries are soft-deletable |
| Idempotency | Repeated subscribe is safe (returns existing or reactivates); state transitions are deterministic |
| Scope & authz | Public 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 / scale | Statistics computed in a single aggregation pass, not row-by-row counting |
| i18n | Language 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
| Concept | Role |
|---|---|
| Subscriber | The mailing-list entry - email (unique), status (active / unsubscribed), language, topics, subscribe and unsubscribe timestamps, and a unique unsubscribe token |
| Statistics summary | A 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 / question | Mitigation / status |
|---|---|
| Duplicate sign-ups from repeated submits | Subscribe is idempotent; email is globally unique |
| Unsubscribe token guessing / replay | Token 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 yet | Forward 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 list | Out of scope; documented as Planned in the URD |
| Statistics cost as the list grows | Single aggregation pass; 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; FR-1..FR-5 built, FR-6 not |
| Rollout | All deployments; no feature flag |
| Migration | None (new entries; no data backfill) |
| Launch criteria | All "verifiable now" criteria (§6, items 1-7) pass; the statistics group (items 8-11) is accepted once the FR-6 screen ships |
| Monitoring | Subscriber 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
- URD: Customer - Newsletter Subscribers
- Related PRD: PRD-CAP-001 - Lead & subscriber capture
- Module: Customer - URD
- Developer: @nx/outreach · Outreach domain model