Engineering reference: the social scheduler architecture
Same system, drawn for engineers. Region, service names, resource identifiers, Bedrock model IDs, Lambda inventory, IAM scopes, the channel-token setup, EventBridge Scheduler config, the DynamoDB schemas, and the approval flow. Read alongside the previous six posts; this one’s the build sheet.
Region and account shape
Default region: ap-southeast-1 (Singapore). Bedrock cross-Region inference, EventBridge Scheduler, and SES outbound are all in good shape there. A second region for multi-region resilience isn’t worth the extra setup work at SMB volume — the failure mode for an SMB is one post going out a few minutes late, not a regional outage. One AWS account dedicated to the scheduler (separate from your other workloads) keeps the IAM blast radius small and lets a single AWS Budgets alarm cover the whole system.
Topology
Lambda functions
All Lambdas use the arm64 architecture, the smallest memory size that meets latency targets (typically 256 MB), Python 3.14 runtime, and CloudWatch Logs at 7-day retention. Each function has its own least-privilege IAM role. None run inside a VPC.
drive-sync— EventBridge Scheduler target, fires every 5 minutes. Uses the Google Drive API + Sheets API (service-account credentials in Secrets Manager underss/drive/sa) to export the posts sheet as CSV and mirror new images from the Drive folder tos3://ss-posts-source/only if the sheet or folder has changed since the last sync. Same pattern syncs the rules and voice docs tos3://ss-rules-source/. Memory: 256 MB. Timeout: 30 s.intake-draft-helper— Lambda Function URL. Triggered when the owner submits a short note from the draft-helper. Reads the brand tone froms3://ss-rules-source/voice.txtand calls Bedrock Haiku 4.5 (anthropic.claude-haiku-4-5-20251001-v1:0viaglobal.anthropic.claude-haiku-4-5-20251001-v1:0) to propose post text per chosen channel, with the per-channel character limit injected into the prompt so the draft fits on the first try. Posts the proposal to the review card with Approve/Edit/Discard. On approve, writes the row to the Drive sheet via the Sheets API with the approved flag unset. Memory: 512 MB. Timeout: 30 s.template-sync— EventBridge Scheduler target, one schedule per recurring template (e.g.cron(0 9 ? * FRI *)for a weekly Friday post). Reads the matching template from the rules doc, drops a fresh row onto the calendar a few days ahead, and surfaces it in the same review card as the draft-helper. Memory: 256 MB. Timeout: 30 s.scheduler— EventBridge Scheduler target, daily at 7am local time (the schedule expression runs inTZ_NAMEset to the SMB’s timezone, e.g.Asia/Singapore). Readss3://ss-posts-source/posts.csvand the rules and voice docs. For each row, computesminutes_to_send, reads state fromss-statusandss-sends, and decides on a move. For queued posts it creates a one-off Scheduler rule for the exact send minute; for due or retry posts it emitsss.sendorss.retrywith the post context as the event payload. Resting posts emit nothing. Memory: 512 MB. Timeout: 60 s. No Bedrock calls.sender— EventBridge rule on thess.sendandss.retryevents. Resolves channels, runs the format check (character limit, link count) and the image check (size, type) against the per-channel rules, fetches the image froms3://ss-posts-source/, refreshes the channel token from Secrets Manager if it’s near expiry, then posts to each channel through its posting endpoint. On a per-channel failure it records the error and lets the scheduler queue a retry; on a format or image failure it flags the post back to the owner and drops it to draft. Writes a row toss-sendsper channel after each attempt. Memory: 512 MB. Timeout: 60 s.approve-handler— Lambda Function URL, public withAuthType: NONE; verifies a signed token on the request body. Triggered by review-card button clicks (Approve/Hold/Send-now). Writes toss-statusandss-audit; on approve, sets the approved flag on the Drive sheet via the Sheets API and books the one-off send; on hold, cancels any queued one-off Scheduler rule and drops the status to draft; on send-now, emits an immediatess.send. Memory: 256 MB. Timeout: 15 s.digest— EventBridge Scheduler target, weekly Sunday 6pm. Readsss-sendsfor the past week and the calendar; sends a digest email summarizing what posted and what’s coming up. No Bedrock; the message is a plain summary table. Memory: 256 MB.recap— EventBridge Scheduler target, monthly on the first Monday at 9am. Reads the past month’sss-sendsandss-audit; calls Bedrock Haiku 4.5 to write a one-paragraph recap narrative; emails it via SES to the configured stakeholder list. Memory: 512 MB.
Storage
- DynamoDB ·
ss-status— one row per post. PKpost_id; attributes:state(draft/queued/sending/sent/failed),queued_for,approved_by,scheduled_at. On-demand. No TTL. - DynamoDB ·
ss-sends— one row per send attempt. PK(post_id, channel); sort keyattempt; attributes:result(ok/fail),posted_url,error,sent_at. On-demand. - DynamoDB ·
ss-audit— one row per write action of any kind. PK(post_id, ts); attributes:action(approve/hold/send-now/draft-helper),by_user,before,after. On-demand. No TTL — this is the long-term audit trail. - S3 ·
ss-posts-source— mirrored CSV from the Drive posts sheet plus the images from the Drive folder. Versioning enabled. Lifecycle to Glacier at 90 days for old images; CSV kept hot. - S3 ·
ss-rules-source— mirrored rules and voice docs as plain text. Versioning enabled. - S3 ·
ss-render-cache— resized image variants per channel, cached so a re-send doesn’t re-process the same image. Lifecycle expiry at 30 days.
Bedrock
- Foundation model.
anthropic.claude-haiku-4-5-20251001-v1:0via the Global cross-Region inference profileglobal.anthropic.claude-haiku-4-5-20251001-v1:0. Two callsites:intake-draft-helperfor roughing out post text, andrecapfor the monthly narrative. A heavier model (anthropic.claude-sonnet-4-6) is wired but unused by default; it’s only worth switching the draft-helper to Sonnet if a business wants longer-form, multi-paragraph posts where the extra reasoning earns its cost. - Embeddings. Not used. The posts are structured rows; deterministic lookup and date arithmetic beat vector retrieval here. No Knowledge Base, no S3 Vectors.
- Quotas. Default account quotas are more than enough at SMB volume. The scheduler itself doesn’t call Bedrock; the draft-helper fires a few times a month at most.
EventBridge Scheduler config
ss-daily-tick—cron(0 7 * * ? *)in the SMB’s timezone. Target:schedulerLambda.ss-drive-sync—rate(5 minutes). Target:drive-syncLambda.ss-template-*— one rule per recurring template, with the template’s own cron. Target:template-syncLambda.ss-weekly-digest—cron(0 18 ? * SUN *)in TZ. Target:digestLambda.ss-monthly-recap—cron(0 9 ? * 2#1 *)(first Monday at 9am) in TZ. Target:recapLambda.- One-off send rules — created on the fly by
schedulerandapprove-handlerwhen a post is queued for its exact minute. Useat(YYYY-MM-DDTHH:MM:SS)expressions with--action-after-completion DELETEso the rule self-cleans. Hold cancels the matching rule by name.
Channels and tokens
- Each channel is registered in the rules doc with its name, posting endpoint, and the per-channel format rules (character limit, accepted image types and sizes, link count).
- Each channel’s long-lived token and refresh credential live in Secrets Manager under
ss/channels/<channel-name>. Thesenderreads the token at send time and refreshes it via the channel’s token endpoint if it’s within the refresh window. - SES outbound for the review cards, the weekly digest, and the monthly recap: verify a sender identity at
scheduler@your-company.comwith DKIM and SPF on the parent domain. Out of sandbox by request.
IAM (least privilege per Lambda)
Each Lambda has its own role with policies scoped to exact ARNs. Sketch:
- scheduler role:
s3:GetObjecton the posts, rules, and voice keys;dynamodb:Query+GetItemonss-status,ss-sends;events:PutEventson the default bus;scheduler:CreateSchedulefor the one-off sends. Nobedrock:*. - sender role:
s3:GetObjecton the image keys ands3:PutObjectonss-render-cache;secretsmanager:GetSecretValueon the channel-token secrets;dynamodb:PutItemonss-sends; outbound network access to each channel’s posting host. - approve-handler role:
dynamodb:PutItemonss-statusandss-audit;secretsmanager:GetSecretValueon the Sheets-API service-account secret; outbound network access tosheets.googleapis.com;scheduler:CreateSchedule+DeleteSchedulefor booking and cancelling one-off sends. - intake-draft-helper role:
s3:GetObjectonss-rules-source;bedrock:InvokeModelon the Haiku ARN;secretsmanager:GetSecretValueon the Sheets-API secret. - drive-sync and template-sync roles:
secretsmanager:GetSecretValueon the Google service-account secret;s3:PutObjecton the posts and rules buckets; outbound network towww.googleapis.com.
Approval flow
The review card is delivered as an HTML email via SES (and, if a business uses Slack, the same card can be posted with Block Kit blocks). Each button carries a signed token in its link that encodes the post_id and the action (approve, hold, send_now). Clicks land on the approve-handler Function URL, which verifies the signature, applies the action, and returns a small confirmation page. Approve and Hold are one-click; Send-now is one-click but still routes the post through the full format check in sender before anything posts. The approval flag on the Drive sheet is the single gate every post passes — a post with the flag unset is never sent, no matter what state the rest of the system is in.
Observability and cost gates
- CloudWatch Logs: all Lambdas, 7-day retention, structured JSON. Subscription filter on
"error"+"throttle"+"timeout"to a CloudWatch metric for alerting. - Alarms: scheduler Lambda failures > 0 in a day (the daily tick is the one piece that has to run); sender failure rate > 1% in 24h; approve-handler signature-verification failures > 5/hour (might mean the signing secret rotated).
- X-Ray: off by default. Not worth the cost at SMB volume.
- AWS Budgets: $15/month threshold, alarm at 80% and 100%, posts to SNS topic
ss-cost-alarmsubscribed to the on-call admin’s email.
Config and secrets
Service-account credentials for Drive and Sheets APIs live in Secrets Manager under ss/drive/sa (one service account with scopes for both APIs). Channel tokens and refresh credentials live under ss/channels/*. The approval-link signing secret is ss/approval/signing-secret. SES sender identity lives in IAM and the verified-domain config. The configured timezone, posting-window hours, blackout days, and account-default channel list all live in Parameter Store under /ss/config/. Lambdas fetch config on cold start and cache for the lifetime of the execution environment.
Deploy
GitHub Actions with OIDC and AWS SAM — no long-lived keys. The opinionated bits: turn on S3 versioning for both ss-posts-source and ss-rules-source so a bad Drive edit can be rolled back in one click; version the EventBridge Scheduler timezone setting so you don’t accidentally start running the daily tick in UTC after a CI rotation; and keep the channel-token secrets in a separate stack so a token rotation doesn’t require redeploying the whole system. Total deployable surface: around eight Lambdas, three DDB tables, three S3 buckets, one EventBridge rule on the default bus (plus the Scheduler rules), and one Budgets alarm.
That’s the full system. Six narrative posts and this engineering reference. If you want to talk about adapting it for your business, see Work with me.
All posts