Building a Serverless SMS Relay for Multi-Carrier Call Forwarding: QDN's Twilio Integration Architecture
What Was Done
Quick Dump Now (QDN) needed to implement cascading phone number forwarding across multiple carriers without carrier-level relay support. The solution: a Twilio-mediated SMS relay that accepts inbound messages on a QDN-owned Twilio number, looks up the destination carrier number from DynamoDB, and forwards the message downstream. This post documents the infrastructure decisions, Lambda architecture, and API Gateway routing that made this possible.
The Problem: Carrier Limitations
QDN's original call-forwarding chain was:
- Customer message → QDN phone line (Sergio's number)
- Sergio's line → backup (858-335-4807)
When tested at the carrier level, this chain wasn't supported by the incumbent carrier. Twilio's programmable SMS API offered an alternative: instead of relying on carrier-level call forwarding, we'd terminate SMS on Twilio, look up the next hop in our own database, and relay via Twilio's API.
Architecture: Lambda + DynamoDB + API Gateway
The implementation uses three AWS primitives working in concert:
1. Inbound SMS Handler Lambda
When Twilio receives an SMS to our QDN Twilio number, it makes an HTTP POST to an API Gateway endpoint. The Lambda function (/Users/cb/Documents/repos/sites/dashboard.quickdumpnow.com/lambda/lambda_function.py) performs:
- Message extraction: Parse Twilio's TwiML request body (From, To, Body, MessageSid)
- Routing lookup: Query DynamoDB table for the destination number keyed by the inbound Twilio number
- Relay send: Use Twilio SDK to send the message to the destination carrier number
- Audit logging: Store original MessageSid, source, destination, and status in DynamoDB audit table for compliance
- TwiML response: Return success/failure TwiML to Twilio's webhook callback
The function signature handles both GET (health checks) and POST (Twilio webhooks):
def lambda_handler(event, context):
http_method = event['httpMethod']
if http_method == 'GET':
return health_check_response()
elif http_method == 'POST':
body = parse_twilio_webhook(event)
destination = lookup_relay_destination(body['To'])
if destination:
relay_result = send_via_twilio(body['From'], destination, body['Body'])
return success_response(relay_result)
else:
return error_response('No relay destination found')
2. DynamoDB Routing Table
A simple key-value table stores forwarding rules:
- Table name:
qdn-sms-routing - Primary key:
TwilioNumber(the inbound Twilio number) - Attributes:
DestinationCarrier(the downstream number),Active(boolean toggle),LastUpdated(ISO timestamp)
This allows operators to update routing rules without redeploying Lambda. A separate maintenance.json seed file (stored in S3) initializes this table on provisioning.
3. API Gateway Routes
Four new routes were added to the QDN API Gateway:
POST /sms/inbound— Twilio webhook target for inbound SMSPOST /sms/relay— Manual relay trigger (for testing or admin override)GET /sms/status— Query relay status and audit logsOPTIONS /sms/*— CORS preflight for cross-origin requests
Each route is backed by the same Lambda but routes through different code paths. The OPTIONS route returns CORS headers without invoking business logic.
Twilio Configuration
Twilio account credentials were stored in /Users/cb/Documents/repos/.secrets/repos.env (mode 600, untracked):
TWILIO_ACCOUNT_SID— Used by SDK to initialize client and verify webhook authenticityTWILIO_AUTH_TOKEN— Primary authentication for runtime API calls (send SMS, fetch message status)- API key pair (type "Main") — Reserved for future admin-only operations (e.g., phone number provisioning, call recording config)
In production, these are injected into Lambda environment variables via CloudFormation, never stored in code.
End-to-End Message Flow
- Customer texts QDN's Twilio number
- Twilio receives SMS, immediately returns 200 OK
- Twilio invokes API Gateway
POST /sms/inboundasynchronously - Lambda extracts sender, message body, and inbound Twilio number
- Lambda queries
qdn-sms-routingDynamoDB table for destination - Lambda calls Twilio SDK's
messages.create(from_=dest_number, to=carrier_number, body=msg) - Twilio sends SMS to destination carrier number via its own SMS provider network
- Lambda logs audit trail (timestamps, MessageSids, direction)
- Destination number receives SMS
Key Infrastructure Decisions
Why DynamoDB Over Hardcoded Routes?
Storing routes in environment variables or Lambda code would require redeployment to change a single number. DynamoDB with TTL support and point-in-time restore provides:
- Dynamic updates without Lambda redeployment
- Audit trail via DynamoDB Streams (future: CloudWatch Logs from stream consumer)
- Easy operator UI (future: dashboard form to update
qdn-sms-routing) - Soft deletes via
Activeboolean flag (don't remove rows, just toggle)
Why Lambda Over EC2/Fargate?
SMS is bursty and unpredictable. Lambda's pay-per-invocation model and cold-start tolerance (Twilio's webhook timeout is 15 seconds; Python cold start averages 1–2 seconds) made it ideal over always-on containers.
Why API Gateway Over Direct Twilio Webhooks?
API Gateway provides:
- Centralized logging and throttling (protect against abusive SMS loops)
- Unified auth model (future: sign requests with AWS SigV4 for internal-only endpoints)
- CORS and request validation before Lambda invocation