PRD: SMS campaigns
| Module | Marketing | PRD ID | PRD-CMP-001 |
| Status | Ready to dev | FEAT | CMP |
| Epic | — | Plane | BANA-1546 |
| Date | 2026-03-13 | Version | v0.1 |
| Packages | @nx/outreach | URD | CMP |
| Surface | Backend | ||
| Owner | Phát Nguyễn | ||
TL;DR
Stands up a real SMS delivery channel for Marketing - a VNPAY provider integration plus an SMS-sending capability in the sign-in flow - proven by its first consumer, OTP request/verify. Live OTP-over-SMS delivery is currently suppressed (non-prod uses a bypass code) pending provider readiness; this is the load-bearing send path the planned campaign-to-segment send (URD-CMP-001) will reuse.
1. Context & Problem
Marketing's Campaigns & Automation feature needs a way to actually deliver messages - and today there is no SMS send path at all. Nothing in the platform can hand a text message to a carrier, so neither authentication (OTP) nor any future campaign can reach a customer's phone. Per the module Non-Goals, BANA integrates an external provider rather than building its own delivery infrastructure.
This increment lays the groundwork - the external provider integration, the capability that uses it, and provider/credential configuration - and exercises it with the first concrete consumer, OTP. The campaign-to-segment send layers on top later; building the channel first unblocks both auth and marketing.
2. Goals & Non-Goals
Goals
- Stand up an SMS provider integration wrapping the VNPAY SMS provider (request building, request/response shapes, a send entry point).
- Add an SMS-sending capability to the sign-in flow and wire SMS sending into the application.
- Load SMS provider configuration (credentials, environment) through central configuration, with idempotent seeding and tolerant migration.
- Build the first consumer - OTP request and verification - with an SMS-backed OTP sender wired in. A log-only sender is implemented for non-prod; phone OTP currently suppresses SMS delivery and accepts a bypass code for testing.
Non-Goals
- The campaign-to-segment send itself (URD-CMP-001) - layered on this channel, not in this increment.
- Triggered / drip automation (URD-CMP-002) and A/B testing & reporting (URD-CMP-003).
- Delivery infrastructure beyond integrating the external VNPAY provider (per module Non-Goals).
- Email / other-channel delivery.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Channel availability | Valid sends return provider acceptance; failures surface, never silently dropped |
| OTP deliverability | OTP request → verify succeeds end to end (live OTP-over-SMS currently suppressed; non-prod uses a bypass code) |
| Config safety | Missing/invalid config never crashes startup; seeding is idempotent across restarts |
| Reuse readiness | A future campaign sender can call the same channel with no provider rework |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Customer (recipient) | Receive an OTP (and later, campaign messages) on their phone |
| End user authenticating | Request and verify an OTP to complete sign-in |
| Owner / Manager (Marketing) | Eventually reuse this channel to reach segments via campaigns |
| Platform operator | Configure VNPAY credentials/environment without redeploying code |
Core scenario: configure the VNPAY SMS provider once → an application flow (OTP request) asks the channel to send → the channel builds the provider request (plaintext by default, or encrypted via the secure-send variant) and dispatches it → the recipient gets the text → OTP verify confirms the code.
5. User Stories
- As an end user authenticating, I want to receive an OTP by SMS and verify it, to complete sign-in securely.
- As a platform operator, I want SMS credentials and environment loaded from configuration, so I can switch providers/environments without a code change.
- As a platform operator, I want missing SMS config tolerated at startup, so environments without SMS still boot.
- As a Marketing owner, I want a reusable SMS channel, so the planned campaign-to-segment send can reach customers without new provider work.
- As a developer, I want a log-only sender in non-prod, so I can exercise SMS flows without spending real messages.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Provide an SMS provider integration exposing an SMS send path (a send entry point plus request/response shapes) | URD-CMP-001 |
| FR-2 | Integrate the VNPAY SMS provider via provider-specific request building | URD-CMP-001 |
| FR-3 | Offer an encrypted secure-send variant (ECDH-derived shared key + AES-256-CBC) for SMS payloads; the default send path (used by OTP) dispatches plaintext | URD-CMP-001 |
| FR-4 | Expose an SMS-sending capability in the sign-in flow that the application calls to send SMS | URD-CMP-001 |
| FR-5 | Load SMS provider config (credentials, environment) via central configuration, seeded idempotently | URD-CMP-001 |
| FR-6 | Tolerate missing SMS configuration during migration (register an empty client instead of failing); the provider config row is always seeded so startup finds a config | URD-CMP-001 |
| FR-7 | Provide OTP request and verification on top of the SMS channel, with an SMS-backed OTP sender wired in; a log-only sender is implemented but not the active wiring - phone OTP currently suppresses SMS delivery and accepts a bypass code in non-prod | URD-CMP-001 |
| FR-8 | Centralize OTP configuration in central configuration; hash OTP codes with one-way hashing | URD-CMP-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 |
|---|---|
| Security | An encrypted secure-send variant is available for SMS payloads (the default/OTP path is plaintext); OTP codes are one-way hashed, never stored in clear |
| Configurability | Provider credentials/environment live in configuration, not code; switchable without redeploy |
| Resilience | Missing SMS config is tolerated during migration (an empty client is registered); failures surface rather than crashing the application |
| Idempotency | Config seeding converges - repeated migrations settle to one record |
| Observability | A log-only sender is implemented for non-prod; in practice phone OTP suppresses delivery and a bypass code makes verify testable without real sends |
| i18n | User-facing OTP messaging is bilingual (English + Vietnamese) |
8. UX & Flows
No merchant-facing screen in this increment; application flows invoke the channel (OTP today, campaign send later). Provider credentials are managed through configuration.
9. Data & Domain
| Concept | Role |
|---|---|
| SMS request / response shape | Shape of an outbound SMS and its provider acknowledgement |
| Provider client configuration | Provider connection settings (credentials, endpoint) |
| SMS provider config | Per-environment provider settings in the integrations configuration area, with an environment setting |
| OTP config | Centralized OTP settings in central configuration |
| OTP record | The issued/hashed OTP used by request + verify |
Conceptual only - full schema and provider detail live in the developer outreach docs.
10. Dependencies & Assumptions
Depends on
- VNPAY SMS provider - the external carrier integration the channel wraps.
- Central configuration - provider/OTP config loading, idempotent seeding, migrations.
- The sign-in service - hosts the SMS-sending capability and the OTP request/verify flow.
Assumptions
- Valid VNPAY credentials and an environment are provided per deployment (or SMS is intentionally disabled).
- One-way hashing is available for OTP secrets.
- A future campaign sender will reuse this channel rather than integrate its own provider.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Campaign-to-segment send (URD-CMP-001) is not yet built on this channel | Channel + OTP shipped first; campaign send is the next increment - PRD stays planned until then |
| Single-provider (VNPAY) coupling | The channel abstracts the provider; a second provider can slot in behind the same capability |
| Missing SMS config could break boot | Migration tolerates absent config (empty client); the config row is always seeded so startup finds one |
| Provider outage / delivery failure | Failures surface to callers; non-prod relies on a bypass code with delivery suppressed |
| Cost of real sends during testing | Phone OTP suppresses delivery and uses a bypass code in non-prod (a log-only sender is also implemented) |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 - SMS delivery channel for Campaigns & Automation |
| Rollout | Channel available where VNPAY config is present; OTP consumer enabled in the sign-in flow |
| Migration | SMS provider config seeded into the integrations configuration area idempotently; environment setting added; tolerant of missing config |
| Launch criteria | OTP request → verify verified end to end (live OTP-over-SMS currently suppressed; bypass code in non-prod); seeding idempotent across migrations; provider config row seeded so startup has a config |
| Monitoring | SMS send success/failure rate, OTP verify success rate, provider error responses |
13. FAQ
Does this send marketing campaigns yet? No. This increment delivers the SMS channel and its first consumer (OTP). The campaign-to-segment send (URD-CMP-001) layers on top in a later increment - which is why this PRD is still planned.
Why is OTP in a Marketing PRD? OTP is auth-facing, but it is the first and load-bearing consumer of the exact channel campaigns will reuse. Proving the channel through OTP de-risks the marketing send.
Which provider does it use? VNPAY SMS, wrapped by the SMS provider integration so it can be swapped or extended.
How are credentials managed? Through central configuration (integrations area), with an environment setting and idempotent seeding - no redeploy to change them.
What happens in non-prod or when SMS isn't configured? Phone OTP suppresses real delivery and accepts a bypass code in non-prod (a log-only sender is also implemented). Missing provider config is tolerated during migration, and the config row is always seeded so startup finds one.
References
- URD: Marketing - Campaigns & Automation - requirements (CMP area)
- Module: Marketing - URD
- Developer: @nx/outreach - capture-side package