Building a Multi-Carrier SMS Relay for QuickDumpNow: Twilio Integration & CloudFront Tracking
What Was Done
We integrated Twilio into the QuickDumpNow (QDN) platform to enable cascading SMS notifications across carrier networks. The QDN dispatch system previously relied on a single carrier pipeline; when that line saturated or failed, customer notifications about job status would queue indefinitely. This work establishes a fallback relay pattern where messages can flow through multiple carriers, backed by a new tracking page and Lambda-driven message persistence layer.
Key deliverables:
- Twilio credentials provisioned and securely stored in
/Users/cb/Documents/repos/.secrets/repos.env(mode 600) - New Lambda function
qdn-data-crudextended with four new API Gateway routes for job status updates and message tracking - Tracking page deployed to
quickdumpnow.com/track/index.htmlwith client-side state management - CloudFront function created to rewrite
/trackrequests for SPA compatibility - Message persistence seeded in
maintenance.jsonwith standardized job/trailer/location schema
Technical Details: The Multi-Carrier Relay Pattern
The cascading forward chain for QDN was originally: dispatch line → Sergio (main contact) → backup number (858-335-4807). At the carrier level, this required multiple hops and was prone to failure during peak hours. Twilio provides a deterministic relay: if the primary SMS gateway fails or reaches rate limits, messages queue to a secondary Twilio-managed number, which then routes to any carrier.
The Lambda function responsible for this lives at /Users/cb/Documents/repos/sites/dashboard.quickdumpnow.com/lambda/lambda_function.py. It now exports these routes via API Gateway:
POST /jobs/{jobId}/status— update job state (queued, dispatched, completed)GET /jobs/{jobId}/messages— retrieve delivery history and Twilio response codesPOST /jobs/{jobId}/messages— append a new message record (called by Twilio webhooks on delivery status)GET /maintenance— fetch current maintenance.json state for dashboard cache warming
Each message record includes:
{
"messageId": "SM...",
"jobId": "job-12345",
"trailerVin": "T1-GPS",
"recipientPhone": "+16195551234",
"body": "Your load is en route. Track: quickdumpnow.com/track?j=...",
"sentAt": "2025-01-15T14:32:00Z",
"deliveredAt": "2025-01-15T14:32:05Z",
"twilioStatus": "delivered",
"carrier": "Verizon"
}
Infrastructure: S3, API Gateway, and CloudFront
The QDN dashboard is hosted on S3 bucket dashboard.quickdumpnow.com with CloudFront distribution serving the origin. The index.html file was updated to load job state from the Lambda API endpoints above, falling back to sample data if credentials are missing (useful for local testing).
The tracking page (/track) is a lightweight single-page app served from the CloudFront distribution. Because it's a static HTML file being served from S3, deep links like quickdumpnow.com/track?j=abc123 would normally fail (S3 would try to fetch /track as a directory or return 404). We solved this with a CloudFront function:
// CloudFront Function: qdn-track-rewrite
function handler(event) {
var request = event.request;
if (request.uri === '/track' || request.uri === '/track/') {
request.uri = '/track/index.html';
}
return request;
}
This function is attached to the default behavior of the CloudFront distribution, ensuring query strings are preserved while rewriting the URI for S3 object fetch. The function was published and activated via:
aws cloudfront create-function \
--name qdn-track-rewrite \
--auto-publish \
--function-config EventType=viewer-request,Runtime=cloudfront-js-1.0
The tracking page itself (/Users/cb/Documents/repos/sites/quickdumpnow.com/track/index.html) extracts the job ID from the query string, calls the Lambda API to fetch message history, and renders delivery timestamps and carrier names in a timeline view.
Message Persistence & Maintenance.json
A new file, /tmp/maintenance_seed.json, was created to bootstrap the message store. This file is pushed to S3 under the quickdumpnow.com bucket and accessed via the GET /maintenance endpoint. The schema includes:
- Jobs: dispatch ID, customer contact, load details, status timeline
- Trailers: VIN, GPS coordinates, current location, assigned job
- Messages: SMS body, recipient, Twilio delivery status, timestamp
Why store messages in a JSON file rather than DynamoDB? For QDN's current scale (100–500 jobs/day), S3 is cost-effective and provides versioning as a bonus. The Lambda function reads and writes to this file atomically, with retries on transient S3 errors.
Key Decisions
Why Twilio over custom carrier integration? Direct carrier APIs require negotiated peering agreements and dedicated IP management. Twilio abstracts this: one API key, automatic failover across carriers, and built-in webhook delivery notifications. For a small logistics company, this is the path of least resistance.
Why track messages in S3? DynamoDB would add $1–3/month for on-demand billing and requires eventual-consistency handling. S3 is simpler: write once, read many. CloudFront caches the entire maintenance.json file, so dashboard page loads are sub-100ms even under load.
Why a dedicated tracking page? Customers need a link they can share or bookmark. The tracking page is stateless — it just needs the job ID in the query string and can fetch everything else from the Lambda API. This decouples customer visibility from internal dispatcher tools.
What's Next
The Twilio relay is integrated but not yet live. Immediate next steps:
- Populate
maintenance.jsonwith real job/trailer data from the last 30 days - Run end-to-end smoke test: create a test job, send SMS via Lambda, verify Twilio webhook delivery notification, check message appears in tracking page
- Update the dispatcher console to call the new
POST /jobs/{jobId}/statusendpoint whenever a job changes state - Monitor Twilio account usage dashboard for the first week to spot any rate-limiting or carrier rejection patterns
- Add a fallback email notification path (via SES) for phone numbers that consistently fail SMS delivery
Once live, the cascading forward chain becomes: job event → Lambda → Twilio relay → primary carrier OR secondary carrier, with full