The short version, for whichever hat you're wearing
- Make the day's content once, send it many times. Generation is a fixed daily cost; delivery is the only part that grows.
- Every step can fail and the morning still happens: retries, fallbacks, and a ladder that ends in plain text.
- Built and tested on a laptop first, then each piece swapped for its AWS twin.
- One package per date, written with a conditional put: reruns are free, double-runs are impossible.
- Outbox + dedupe key (subscriber, date, channel): a crash never sends twice.
- Scripture is never generated: it's read from a stored, versioned text; models only write around it.
- Small services behind queues, each with one job, its own data and its own failure budget.
- Same contracts locally and in the cloud: LocalStack, MinIO and Mailpit stand in for SQS, S3 and SES.
- Per-channel pacing and circuit breakers, so a slow WhatsApp never delays email.
- The promise is "7 AM, every day": on-time delivery rate is the headline metric.
- Four languages, one review gate, with a native-speaker check before any language goes live.
- Cost is mostly messaging, not AI; the slider in section 12 shows where it goes.
What lands at 7 AM
The screens
Eleven small services
Each does one job, owns its data, and talks through queues. Press play to follow one morning.
Laptop first, then AWS
Every service talks to an interface, not a vendor. Locally, a container plays each AWS part. Flip it:
services:
postgres: { image: postgres:16 }
localstack: { image: localstack/localstack, environment: { SERVICES: sqs,sns,dynamodb,stepfunctions,events } }
minio: { image: minio/minio, command: server /data }
mailpit: { image: axllent/mailpit } # every email lands here, viewable at :8025
wa-mock: { image: wiremock/wiremock } # fake WhatsApp Cloud API: records, can return 429s
reading: { build: ./services/reading }
writer: { build: ./services/writer, environment: { LLM_MODE: fixtures } }
# ... translate, image, audio, video, fanout, sender-email, sender-whatsapp, subscribers
Make it once, in parallel
Run it twice, get one morning
Seven o'clock, everywhere
One package, sent in waves as 7 AM sweeps around the world. A schedule per time zone fires at 7 AM local and fans out only that zone's subscribers. (SQS can only delay a message 15 minutes, so the queue can't do the waiting.)
Pace, don't burst
WhatsApp and email providers cap messages per second. Blast them and you get throttled; pace them and nothing fails.
A video, no matter what
Never twice, never lost
Checks before anything sends
The rule that matters most: models never write scripture. The text comes from a stored, versioned Bible; models only write around it.
One number: on time
Where the money goes
Rebuild it yourself
- Load the Bible once. Public-domain text per language into a
versestable; a liturgical calendar maps each date to a reading. - Write each service against an interface:
Queue,Blob,KV,Mailer,WhatsApp,LLM. One local and one AWS implementation each. - Run the stack with compose, models in fixture mode, then real calls. Mailpit shows every email; the WhatsApp mock can return 429s on demand.
- Test the failures, not just the path: kill containers mid-run, fire the scheduler twice, make the mock throttle.
- Port one piece at a time: containers to ECS Fargate, LocalStack to SQS/SNS/DynamoDB, MinIO to S3, Postgres to Aurora, Mailpit to SES, the mock to the WhatsApp Cloud API, all in CDK.
- Canary first: one time zone and one language for a week, watching the on-time rate, then everyone.
daily-bible/
services/ reading writer translate image audio video
fanout sender-email sender-whatsapp subscribers orchestrator
libs/ ports/ (Queue, Blob, KV, Mailer, WhatsApp, LLM) adapters/local adapters/aws
infra/ docker-compose.yml cdk/ (VPC, ECS, SQS, DynamoDB, Aurora, SES, Step Functions)
data/ bible/.sqlite calendar/ quotes.yaml (saints, sourced)
tests/ contract/ chaos/ load/ { "key": "sub_8f2c:2026-11-05:whatsapp",
"package": "s3://daily/2026-11-05/",
"lang": "mr", "channel": "whatsapp",
"zone": "Asia/Kolkata" }Machine translation has opinions. Ask for "the Good Shepherd" in Marathi with no context, and you can get a shepherd who's simply good at his job. That's why every language has a native-speaker gate.
Decisions, one page
- One package or one per subscriber?per subscriberone per date
AI cost stays flat. Cost: no personalisation yet.
- Monolith or services?one appsmall services + queues
A slow renderer can't stall sending. Cost: more to deploy.
- Where does scripture come from?the modela stored, versioned text
Never misquote the Bible. Cost: one-time loading.
- How do sends get timed?delayed queue messagesa release schedule per time zone
Queues only delay 15 minutes; schedules can wait hours. Cost: ~40 schedules to manage.
- How do retries stay safe?hopededupe key + idempotency key
Crashes never double-send. Cost: a write per message.
- What if there's no video?skip itthe ladder
Something always arrives. Cost: a renderer to run.
- How is it tested?in the cloudlocally, failures included
Fast loops, no bill. Cost: keeping mocks honest.
The well-earned drink
Every Brew Miles trip ends with one:
- Make it once, send it many times.
- Claim the date, checkpoint every stage, and reruns become free.
- Key every message, pace every channel, and keep a breaker per provider.
- Build a ladder whose last rung is plain text.
- Test it on a laptop, failures and all, then port one piece at a time.
Show the docs this design leans on
- Step Functions error handling and SQS dead-letter queues (AWS docs).
- Making retries safe with idempotent APIs and timeouts, retries and backoff with jitter (AWS Builders' Library).
- The transactional outbox pattern (microservices.io).
- WhatsApp Cloud API (Meta) and SES sending quotas (AWS docs).
- LocalStack, MinIO and Mailpit, for the local stack.
- Circuit breaker (Martin Fowler).