PRD: Lead & subscriber capture
| Module | Marketing (EXT-02) | PRD ID | PRD-CAP-001 |
| Status | Shipped | Owner | Marketing squad |
| Date | 2026-04-03 | Version | v1.0 |
| Packages | @nx/outreach · @nx/core | URD | CAP |
TL;DR
Lets the public marketing site collect interested leads - contact/sales/demo inquiries and newsletter subscribers - and gives admins a live read of the subscriber base. Subscribing is idempotent by email, unsubscribe is honored via a token, and an admin statistics endpoint reports totals, monthly growth, and the active/deactivated split. The outcome: a clean, deduplicated audience that respects opt-out, ready to power the campaigns planned for Phase 2.
1. Context & Problem
Merchants want to grow an audience from their public site - visitors who request a demo, contact sales, or opt into the newsletter. Without a capture surface, those signals are lost or scattered across ad-hoc channels: there is no deduplicated subscriber list, no enforced unsubscribe, and no way for an admin to gauge how the audience is growing. That gap blocks any future outbound campaign work, which depends on a clean, opt-out-respecting subscriber base.
This feature establishes the capture foundation in @nx/outreach: it records inquiries and newsletter opt-ins, keeps the subscriber list idempotent and opt-out-safe, and exposes an admin statistics read over the global subscriber list.
2. Goals & Non-Goals
Goals
- Capture inquiries (contact/sales/demo) submitted from the public site, with a real-time notification to admins.
- Make newsletter subscribe idempotent by email - re-subscribing reactivates a previously unsubscribed record rather than duplicating it.
- Honor token-based unsubscribe before any reactivation or send.
- Expose subscriber statistics for admins: total, new-this-month, activated, and deactivated counts.
Non-Goals
- Campaigns, drip automation, and A/B testing - owned by Campaigns & Automation (
CMP, P2). - Email/SMS delivery infrastructure - provided by external providers.
- Promotional pricing - owned by the Campaign module.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Deduplication | Zero duplicate active subscribers per email |
| Opt-out integrity | 100% of unsubscribe tokens resolve to a deactivated record; no send to deactivated subscribers |
| Capture latency | Inquiry submit → admin notification is near real-time |
| Audience visibility | Statistics endpoint returns total / monthlyNew / activated / deactivated for the global list |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Visitor | Submit an inquiry or subscribe to the newsletter from the public site |
| Subscriber | Unsubscribe at any time via a one-click token link |
| Admin / Manager | See the subscriber list and its statistics (total, growth, active/deactivated) |
Core scenarios: a visitor submits an inquiry (admin is notified in real time) or subscribes to the newsletter (idempotent by email) → a subscriber later clicks the unsubscribe link (token-resolved, deactivated) → an admin reads subscriber statistics over the global list.
5. User Stories
- As a visitor, I want to submit a contact/sales/demo inquiry from the public site, so the business can follow up.
- As a visitor, I want to subscribe to the newsletter with my email, so I receive updates - and subscribing twice never creates a duplicate.
- As a subscriber, I want to unsubscribe via a token link, so I stop receiving messages.
- As a previously-unsubscribed visitor, I want re-subscribing to reactivate my record, so I rejoin without duplicate entries.
- As an admin, I want subscriber statistics (total, new-this-month, activated, deactivated), so I can gauge audience growth.
- As an admin, I want a real-time notification when an inquiry arrives, so I can respond quickly.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Capture an inquiry (contact/sales/demo) submitted from the public site | URD-CAP-001 |
| FR-2 | Newsletter subscribe is idempotent by email; an active email returns the existing record, no duplicate | URD-CAP-002 |
| FR-3 | Re-subscribing a previously unsubscribed email reactivates it (subscribedAt reset, unsubscribedAt cleared, status ACTIVATED) | URD-CAP-002 |
| FR-4 | Token-based unsubscribe resolves a subscriber by unsubscribeToken and deactivates it | URD-CAP-003 |
| FR-5 | Admin subscriber statistics: total, monthlyNew, and the activated / deactivated split, via a single aggregation over the global subscriber list | URD-CAP-004 |
| FR-6 | A new inquiry submission emits a real-time notification to admins | URD-CAP-001 |
Full requirement text and acceptance criteria live in the Marketing URD. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | Subscribe is idempotent by email - one active record per email; soft-deleted rows excluded from statistics |
| Opt-out integrity | Unsubscribe is honored before any reactivation or send; deactivation is via token only |
| Tenancy | Capture tables are global (not merchant-scoped) per module constraint C-02; statistics aggregate across all subscribers |
| Authz | Admin reads (list, statistics) are permission-gated; public capture endpoints accept JWT/BASIC strategies |
| Performance | Statistics is a single COUNT(*) FILTER (...) aggregation query over the subscriber list |
| i18n | Subscriber locale is captured so future sends can localize; user-facing labels are bilingual ({ en, vi }) |
8. UX & Flows
Key screens: the public-site capture forms (inquiry + newsletter) on the marketing site, and the back-office subscriber list screen (overture-apps/bo) with form-validation UI.
9. Data & Domain
| Entity | Role |
|---|---|
Inquiry | A contact/sales/demo request captured from the public site (type, name, email, business, subject) |
Subscriber | A newsletter opt-in - email, locale, topics, status (ACTIVATED/DEACTIVATED), subscribedAt, unsubscribedAt, unsubscribeToken |
Conceptual only - full schema and invariants in the outreach developer docs.
10. Dependencies & Assumptions
Depends on
@nx/outreach- the capture package hosting inquiry and subscriber controllers, services, and repositories.@nx/core- shared subscriber/inquiry schemas and the core repositories.- Real-time notification channel - the outreach WebSocket service that pushes inquiry-submitted events to admins.
Assumptions
- Capture tables are global (constraint C-02); there is no per-merchant scoping on subscribers/inquiries.
- Each subscriber holds a unique
unsubscribeTokenissued at capture time for one-click unsubscribe. - External providers (not in scope) handle eventual email/SMS delivery.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Duplicate subscribers on rapid double-submit | Subscribe is idempotent by email - existing active record returned, no insert |
| Sending to an unsubscribed address | Unsubscribe deactivates the record; constraint C-01 requires opt-out be honored before any send |
| Global (non-scoped) tables across merchants | Accepted per constraint C-02; statistics are intentionally global |
| Statistics cost as the list grows | Single aggregation query today; revisit indexing/caching if the list grows large |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 (foundation) - marketing/CAP in the URD feature catalog |
| Rollout | All sites; no feature flag |
| Migration | None (new entities; global tables) |
| Launch criteria | Idempotent subscribe verified (no duplicates); token unsubscribe deactivates; statistics return total/monthlyNew/activated/deactivated; inquiry submit fires admin notification |
| Monitoring | Subscribe/unsubscribe volume, duplicate-subscriber rate (expect 0), statistics query latency, inquiry notification delivery |
13. FAQ
What happens if the same email subscribes twice? Nothing duplicates - an already-active email returns the existing record; a previously unsubscribed email is reactivated.
How does unsubscribe work? Via a token link: the unsubscribeToken resolves to the subscriber, which is then deactivated. Opt-out is always honored before any send.
Are subscribers scoped per merchant? No - capture tables are global per constraint C-02, so statistics aggregate across all subscribers.
What does the statistics endpoint return? A single aggregation: total, monthlyNew (subscribed since the start of the current month), and the activated / deactivated split, excluding soft-deleted rows.
Does this send any campaigns? No - sending is out of scope here and owned by Campaigns & Automation (CMP, P2). This feature only captures and reports the audience.
References
- URD: Marketing - Lead & Subscriber Capture
- Related PRD: Campaigns & Automation (planned)
- Module: Marketing - overview + traceability
- Developer: @nx/outreach