```html

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 SMS
  • POST /sms/relay — Manual relay trigger (for testing or admin override)
  • GET /sms/status — Query relay status and audit logs
  • OPTIONS /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 authenticity
  • TWILIO_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

  1. Customer texts QDN's Twilio number
  2. Twilio receives SMS, immediately returns 200 OK
  3. Twilio invokes API Gateway POST /sms/inbound asynchronously
  4. Lambda extracts sender, message body, and inbound Twilio number
  5. Lambda queries qdn-sms-routing DynamoDB table for destination
  6. Lambda calls Twilio SDK's messages.create(from_=dest_number, to=carrier_number, body=msg)
  7. Twilio sends SMS to destination carrier number via its own SMS provider network
  8. Lambda logs audit trail (timestamps, MessageSids, direction)
  9. 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 Active boolean 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