Engineering reference: the missed-call text-back architecture
This is the missed-call text-back with the friendly labels removed: the real resource names, the runtime, the table key schemas, the single public Function URL, the escalation schedule, and the IAM scope. If you want to build it rather than understand it, start here.
Key takeaways
- Six Lambda functions, all Python 3.14 on arm64, wired through one SQS queue with a dead-letter queue.
- One public surface: a single Lambda Function URL on
mctr-webhookthat takes both missed-call events and inbound replies — no API Gateway. - Five DynamoDB tables, all on-demand: calls, threads, customers mirror, opt-out list, and an append-only audit log.
- One EventBridge Scheduler with two rules: the 15-minute no-reply sweep and the customer-sheet sync.
- One Bedrock model, Claude Haiku 4.5 via Global cross-Region inference, called only by the replier. Single region,
eu-west-2.
The architecture, for engineers
This is the same system as Part 1 with the friendly labels removed and the real resources named. Everything is in one region, eu-west-2 (London), in one account. There is no API Gateway, no NAT Gateway, and nothing always-on; the only inbound surface is one Lambda Function URL, outbound SMS goes through SNS, and work is buffered on a single SQS queue.
mctr-webhook, an SQS-buffered set of six Lambdas, five DynamoDB tables, Bedrock called only by the replier, SNS for SMS and SES for handover, and two scheduled jobs. One region, one account, no API Gateway.Lambda functions
Six functions, all Python 3.14 on arm64, all with CloudWatch Logs at 7-day retention. Each does one job and hands off; the SQS queue (mctr-jobs, with mctr-jobs-dlq as its dead-letter queue after five attempts) decouples the public webhook from the slower model and SMS calls.
mctr-webhook— the only public surface. Backs the single Lambda Function URL and handles both event types from the provider. For a missed call: verifies the signature, de-duplicates againstmctr-callswith a conditional write, checksmctr-optoutand quiet hours, matches the caller againstmctr-customers, and enqueues a compose job. For an inbound reply: enqueues a routing job. Nothing slow happens here.mctr-replier— SQS-triggered on compose jobs. Makes the single Bedrock call, validates the draft, injects the booking link, sends one SMS via SNS, and writes the result tomctr-calls(statetexted) andmctr-audit.mctr-router— SQS-triggered on routing jobs. Matches the reply to its open call inmctr-calls, appends tomctr-threads, handlesSTOPby writingmctr-optout, and assigns or escalates by keyword-and-recency triage.mctr-escalator— builds the handover (call + matched customer + text-back + reason), de-duplicates against open threads, and emails the support inbox via SES.mctr-drive-sync— scheduled. Pulls the customer sheet from Drive and upserts rows intomctr-customers.mctr-sweep— scheduled. Queriesmctr-callsfortextedcalls past the no-reply window with no reply and not yet escalated, hands each tomctr-escalator, and marks the callescalatedso it’s only flagged once.
Data stores, schedules, and messaging
- DynamoDB (all on-demand).
mctr-calls— PKphone(E.164), SKcall_ts; one item per missed call with its dedup marker, text-back, and state (texted/replied/escalated), queried newest-first to match replies.mctr-threads— PKthread_id, the conversation and assignment state.mctr-customers— PKphone, the directory mirrored from the sheet.mctr-optout— PKphone, the STOP suppression list checked before every send.mctr-audit— PKcall_id, SKts, append-only, holding each text-back and the facts it was built from. - Function URL. One, on
mctr-webhook, with provider signature verification in-function;AuthType NONEat the edge because authenticity is enforced by the shared secret, not by IAM. No API Gateway. - SNS and SES. SNS sends the outbound text-back (or the provider’s own SMS API does, if you route SMS through them); SES sends escalation and handover email to the team from a verified domain with DKIM.
- EventBridge Scheduler. Two rules —
mctr-sweepatrate(15 minutes)gated to opening hours, andmctr-drive-syncatrate(15 minutes). - Secrets Manager. Two secrets — the webhook signing secret and the SMS/provider API key — fetched at call time, never in env vars or the sheet.
- Bedrock. Model id
anthropic.claude-haiku-4-5via the Global cross-Region inference profile, invoked only bymctr-replier.
IAM scope and region
Each function gets its own execution role scoped to exactly what it touches, no wildcards. mctr-webhook can read mctr-customers and mctr-optout, conditionally write mctr-calls, read the signing secret, and send to mctr-jobs — it cannot call Bedrock or SNS. mctr-replier is the only role with bedrock:InvokeModel, scoped to the one Haiku profile; it can publish to SNS and write mctr-calls and mctr-audit, but cannot delete from any table. mctr-router can write mctr-threads and mctr-optout and invoke the escalator, nothing more. mctr-escalator can send via SES and read its inputs only. The scheduled functions hold the narrow Drive and table permissions they need and no inbound surface at all. Everything runs in eu-west-2; the only cross-Region path is Bedrock’s Global inference profile, which routes the model call for capacity and is not a data store. An AWS Budgets alarm watches the monthly spend — with SMS the line most likely to move, it’s the cheapest early warning that volume (or a loop) is running hot.
Design rules that shaped the build
- One job per function. Six small Lambdas beat one that does everything; the queue decouples the slow calls from the webhook.
- One public surface. Only
mctr-webhookis reachable from outside, on a single Function URL, authenticated by a shared secret. - Least privilege, per role. Only the replier can call Bedrock and SNS; only the webhook and router touch the opt-out list.
- State in DynamoDB. Tables for calls, threads, customers, opt-out, and audit; the calls table’s state field drives the sweep.
- One region, one model.
eu-west-2throughout; Bedrock Haiku 4.5 via Global inference, called once per text-back. - A budget alarm is a smoke detector. With SMS the variable line, a Budgets alert is the cheapest way to catch a runaway.