Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from flask import Flask, redirect, render_template, request

import notifications
import sms_notifications
import storage
from db import get_db_connection
from feature_flags import is_feature_enabled
Expand Down Expand Up @@ -106,6 +107,28 @@ def cron():
conn.execute("SELECT 1")
print("Hello from cron job")

@app.route("/sms-notifications", methods=["GET", "POST"])
def sms_notifications_page():
logger.info("Accessed /sms-notifications endpoint with method %s", request.method)
if request.method == "POST":
phone_number = request.form.get("phone_number")
message = "This is a test SMS from Strata Platform."

# Check opt-out status first
opt_out_status = sms_notifications.check_opt_out_status(phone_number)
if opt_out_status.get("opted_out"):
return f"Cannot send SMS: {phone_number} has opted out of messages."

# Send SMS
result = sms_notifications.send_sms(phone_number, message)
logger.info("SMS send result: %s", result)

if result["success"]:
return f"SMS sent successfully to {phone_number}. Message ID: {result['message_id']}"
else:
return f"Failed to send SMS: {result['error']} (Code: {result.get('error_code', 'Unknown')})"

return render_template("sms_form.html")

if __name__ == "__main__":
main()
90 changes: 90 additions & 0 deletions app/sms_notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import logging
import os
import boto3
import json
from botocore.exceptions import ClientError

logging.basicConfig()
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def send_sms(phone_number, message, message_type="TRANSACTIONAL"):
"""
Send SMS via AWS End User Messaging (PinpointSMSVoiceV2)
"""
try:
logger.info("Initializing AWS Pinpoint SMS Voice V2 client")
client = boto3.client("pinpoint-sms-voice-v2")
phone_pool_id = os.environ.get("AWS_SMS_PHONE_POOL_ID")
configuration_set = os.environ.get("AWS_SMS_CONFIGURATION_SET_NAME")
logger.info("Sending SMS Using Configuration Set: %s", configuration_set)
logger.info("Sending SMS Using Phone Pool ID: %s", phone_pool_id)

params = {
"DestinationPhoneNumber": phone_number,
"OriginationIdentity": phone_pool_id,
"MessageBody": message,
"MessageType": message_type
}

# Add configuration set for tracking
if configuration_set:
params["ConfigurationSetName"] = configuration_set

# Add context for tracking (optional)
params["Context"] = {
"ApplicationName": "template-app",
"Environment": "dev"
}

if check_opt_out_status(phone_number).get("opted_out"):
logger.warning("Phone number %s has opted out of SMS messages. Aborting send.", phone_number)
return {
"success": False,
"error": f"Phone number {phone_number} has opted out of SMS messages."
}

response = client.send_text_message(**params)

return {
"success": True,
"message_id": response.get("MessageId"),
"response": response
}

except ClientError as e:
error_code = e.response["Error"]["Code"]
error_message = e.response["Error"]["Message"]
logger.error(f"ClientError: {error_code} - {error_message}")

return {
"success": False,
"error": error_message,
"error_code": error_code,
"details": str(e)
}
except Exception as e:
return {
"success": False,
"error": str(e)
}

def check_opt_out_status(phone_number):
"""
Check if a phone number is opted out
"""
try:
client = boto3.client("pinpoint-sms-voice-v2")

response = client.describe_opted_out_numbers(
OptOutListName="default",
OptedOutNumbers=[phone_number]
)

opted_out_numbers = response.get("OptedOutNumbers", [])
is_opted_out = any(num["OptedOutNumber"] == phone_number for num in opted_out_numbers)

return {"opted_out": is_opted_out}

except Exception as e:
return {"error": str(e)}
1 change: 1 addition & 0 deletions app/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ <h1>Hello, world</h1>
<li><a href="/migrations">Migrations</a></li>
<li><a href="/document-upload">Document Upload</a></li>
<li><a href="/email-notifications">Email Notifications</a></li>
<li><a href="/sms-notifications">SMS Notifications</a></li>
<li><a href="/feature-flags">Feature flags</a></li>
<li><a href="/secrets">Secrets</a></li>
</ul>
Expand Down
24 changes: 24 additions & 0 deletions app/templates/sms_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>SMS Test</title>
</head>
<body>
<h1>Send Test SMS</h1>
<form method="post">
<div>
<label for="phone_number">Receiver Phone Number (E.164 format: +1234567890):</label><br>
<input type="tel" id="phone_number" name="phone_number"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make this a select element populated by the rendering endpoint, with only the predefined simulator numbers by default.

(longer term we should probably better lock down the email testing endpoint as well, we don't really want to be a vehicle to spam people)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restricting this to the simulator numbers here will prevent from testing approved phone numbers. I added an extra message to this form, specifying the phone numbers that can be used for testing if using simulator phone number as originator.

pattern="^\+[1-9]\d{1,14}$"
placeholder="+1234567890"
required><br>
<small>Must include country code (e.g., +1 for US)</small><br>
<small><strong>If using simulator phone numbers for testing:</strong><br>
• +14254147755 (Success Text testing)<br>
• +14254147167 (Text Blocked testing)</small>
</div>
<br>
<button type="submit">Send Test SMS</button>
</form>
</body>
</html>
113 changes: 113 additions & 0 deletions docs/decisions/infra/2026-02-19-sms-notifications-implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# ADR: Enable SMS Notifications in Strata AWS Infrastructure Template via AWS End User Messaging Phone Number Pool

- **Status:** Proposed (Draft)
- **Date:** 2025-01-28
- **Author:** Johan Robles
- **Related Ticket:** #976

---

## Context and Problem Statement

The Nava Strata AWS Infrastructure template currently supports Email notifications. As part of expanding multi-channel communication capabilities, the team is introducing SMS notifications into the Strata offering.

At the infrastructure layer, this feature leverages AWS End User Messaging (SMS), which introduces several operational and provisioning considerations that impact how SMS is enabled within the Nava Strata Infrastructure Template:
- Arbitrary phone numbers cannot be used. Phone number registration and approval is a manual AWS-managed external process that may take approximately 1–15 days depending on region and use case.
- AWS recommends managing approved originators through a Phone Number Pool. Benefits for this solution:
- Automatic failover if an originator fails - Originator phone numbers are controlled and managed by external phone carriers (not AWS)
- Rotation of numbers without application code changes
- Ability to temporarily include simulator numbers for development
- The current Terraform AWS Provider has limited support for SMS Voice v2 resources:
- Event Destinations (e.g., `TEXT_DELIVERED`, `TEXT_BLOCKED`, `TEXT_FAILURE`) cannot be fully configured and linked to a Configuration Set using Terraform. Carrier-level delivery events are necessary to understand actual delivery outcomes (which differ from API-level success responses).
- Phone Number Pools cannot be provisioned using the Terraform AWS Provider.
- Because of these provider limitations, certain resources must be provisioned using AWS CloudFormation, invoked from Terraform via `aws_cloudformation_stack`.

## Decision Outcome

Implement SMS notification enablement using **Infrastructure-Provisioned Phone Number Pool via CloudFormation definition within Terraform (Option 4)**, including:

1. A new infrastructure module: `notifications-sms`
2. CloudFormation-managed SMS resources (via Terraform)
3. A Phone Number Pool module `notifications-phone-pool` with associated phone numbers
4. Carrier-level delivery event logging to CloudWatch
5. IAM policies scoped to the Phone Pool ARN (least privilege)
6. Conditional VPC Interface Endpoint for `sms-voice`
7. Standardized outputs for application integration

## Considered Options

### Option 1 — Basic SMS Enablement

Provision:
- VPC Interface Endpoint
- Configuration Set
- IAM permission for `sms-voice:SendTextMessage`

**Pros**
- Simplest implementation
- Fully supported via Terraform AWS Provider

**Cons**
- Application teams manage phone number resources
- Requires broad IAM Access policy permissions
- No carrier-level delivery visibility

### Option 2 — Add Carrier-Level Delivery Monitoring

Builds on Option 1 and adds:
- Event destinations for:
- `TEXT_DELIVERED`
- `TEXT_BLOCKED`
- `TEXT_FAILURE`
- Other asynchronous carrier responses

**Pros**
- Delivery visibility
- Enables reliability improvements (e.g., the possibility of adding message retry mechanism)

**Cons**
- Requires CloudFormation integration which increase implementation complexity

### Option 3 — Infrastructure-Provisioned Single Phone Number

Builds on Option 2 and provisions a single originator number.

**Pros**
- App teams do not manage phone numbers (just the external registration process)
- Application IAM Access Policy can be restricted to one number which improves security posture

**Cons**
- Any testing is blocked until originator phone number approval
- Originator phone number rotation requires Terraform changes
- Single number increases operational risk

### Option 4 — Infrastructure-Provisioned Phone Number Pool (Selected)

Builds on Option 2 and provisions:

- Phone Number Pool
- Associated phone number (When phone number registration is approved)
- Optional simulator phone number is a provisioned for development purpose

**Pros**
- Aligns with AWS best practices
- Supports number rotation without code changes
- Application IAM Access Policy scoped to pool ARN (least privilege)
- Simulator Phone Number support for development - no need to wait for originator phone number approval for basic testing.
- Reduced operational risk - Multiple phone numbers can be added to the pool

**Cons**
- Higher infrastructure complexity
- Requires CloudFormation integration

## Rationale

Option 4 provides:

- Strong security posture through least-privilege IAM
- Improved operational resilience via number pooling
- Carrier-level delivery observability
- Development/testing flexibility via simulator phone number
- Alignment with AWS best practices

It balances reliability, security, and observability while managing Terraform provider limitations.
Loading
Loading