Engineering reference: the contract summarizer architecture
Same system, drawn for engineers. Region, service names, resource identifiers, Bedrock model IDs, Lambda inventory, IAM scopes, the SES inbound rule set, the S3 Vectors index, 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). SES inbound, Textract, Bedrock cross-Region inference, and S3 Vectors are all available there. A second region for resilience isn’t worth the setup at SMB volume — the failure mode is a contract that has to be re-forwarded, not a regional outage. One AWS account dedicated to the summarizer (separate from 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 (Graviton), the smallest memory size that meets latency targets, 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-watch— EventBridge Scheduler target, fires every 5 minutes. Uses the Google Drive API (service-account credentials in Secrets Manager undercs/drive/sa) to list new files in the watched folder and copy each tos3://cs-contracts/<uuid>. Records the Drive file id incs-jobsto avoid re-importing. Memory: 256 MB. Timeout: 30 s.intake-ses— S3 PUT trigger ons3://cs-raw-mime/. Walks the MIME tree, extracts the contract attachment (PDF/PNG/JPEG/TIFF go straight to Textract; DOCX falls back topython-docx, XLSX toopenpyxl), and copies it tos3://cs-contracts/. Both fallback packages are stable and widely used in 2026, though maintenance velocity is light — acceptable for a path that runs a few dozen times a month;python-docx-ossis a drop-in if extraction precision becomes a concern. Memory: 512 MB. Timeout: 60 s.esign-webhook— Lambda Function URL,AuthType: NONE; verifies an HMAC signature from the e-sign provider (DocuSign Connect, PandaDoc webhooks, etc.) using the secret incs/esign/hmac. On a completed-envelope event, fetches the signed PDF via the provider API and copies it tos3://cs-contracts/. Memory: 256 MB. Timeout: 30 s.reader— S3 PUT trigger ons3://cs-contracts/. The core function. Runs TextractStartDocumentTextDetection+StartDocumentAnalysisasynchronously for multi-page contracts; on completion (SNS notification) reads the structured text and splits it into numbered clauses with a deterministic Python pass. Calls Bedrock Haiku 4.5 (anthropic.claude-haiku-4-5-20251001-v1:0viaglobal.anthropic.claude-haiku-4-5-20251001-v1:0) to fill the fixed term shape, validating that each field cites an existing clause. Embeds each clause with Titan Text Embeddings V2 (amazon.titan-embed-text-v2:0, 1024-dim) and upserts to thecs-clausesS3 Vectors index; runs the always-flag topic search; calls Bedrock Sonnet 4.6 (anthropic.claude-sonnet-4-6-20250930-v1:0viaglobal.anthropic.claude-sonnet-4-6-20250930-v1:0) on matched clauses for the risk flags; validates every flag quotes real clause text; writes the summary draft tos3://cs-summaries/and a row tocs-jobs. Memory: 1024 MB. Timeout: 120 s.deliver— EventBridge rule on thecs.summarize_doneevent. Posts the draft to the configured surface: a Drive folder (Drive API), a Slack channel (chat.postMessagewith Block Kit buttons), or an email (SESSendRawEmail), each with the not-legal-advice banner and the Approve / Send-to-a-human / Hold actions. Memory: 256 MB. Timeout: 30 s.approve-handler— Lambda Function URL,AuthType: NONE; verifies a Slack signature (or a signed token for email-link clicks). Triggered by the three review buttons. Writes tocs-jobsandcs-audit; on approve, files the summary against the contract version and delivers it; on send-to-a-human, forwards the draft and quoted clauses to the configured lawyer/manager via SES and marks the jobawaiting_human; on hold, parks the draft. Memory: 256 MB. Timeout: 15 s.summary-monthly— EventBridge Scheduler target, first Monday 9am. Reads the past month’scs-jobsandcs-audit; calls Bedrock Haiku 4.5 to write a one-paragraph rollup (how many contracts read, how many high-stakes flags, how many sent to a human); emails it via SES to the configured stakeholders. Memory: 512 MB.
Storage
- DynamoDB ·
cs-jobs— one row per contract read. PKcontract_id; sort keysummary_version; attributes:source_lane(drive/ses/esign),status(reading/drafted/approved/awaiting_human/held),page_count,flag_count,high_stakes(bool),created_at. On-demand. - DynamoDB ·
cs-audit— one row per review action. PK(contract_id, ts); attributes:action(approve/send_to_human/hold),by_user,summary_version,notes. On-demand. No TTL — long-term audit trail. - S3 ·
cs-contracts— raw contracts, one object per upload. Versioning enabled. Lifecycle to Glacier at 90 days; expiry at 7 years. - S3 ·
cs-summaries— the cleaned clause text and the one-page summary drafts/finals, versioned, so any summary can be pulled up exactly as it read when approved. - S3 ·
cs-raw-mime— raw inbound MIME from the forwarding lane. Lifecycle to Glacier at 30 days; expiry at 7 years. - S3 Vectors ·
cs-clauses— clause embeddings (1024-dim Titan V2), keyed by(contract_id, clause_no)with the clause text as metadata. Used by the always-flag topic search; entries for a superseded contract version are pruned on re-upload.
Bedrock
- Foundation models.
anthropic.claude-haiku-4-5-20251001-v1:0for the term pull and the monthly rollup, andanthropic.claude-sonnet-4-6-20250930-v1:0for the risk read — both via their Global cross-Region inference profiles (global.anthropic.*). Haiku is the cheap path; Sonnet is reserved for the few matched clauses where reasoning earns its cost. - Embeddings.
amazon.titan-embed-text-v2:0(1024-dim) for clause embeddings, stored in thecs-clausesS3 Vectors index. This is the one system in the philosophy that genuinely needs retrieval — the flag search has to find topically-matching clauses regardless of the contract’s wording. - Quotas. Default account quotas are more than enough at SMB volume. Both models fire at most twice per contract; at a couple hundred contracts a month that’s well within limits.
Grounding and guardrails
- Citation enforcement. The
readervalidates, in plain Python, that every term field cites an existing clause number and every risk flag quotes text that literally appears in its clause. Unsupported fields are blanked; unsupported flags are dropped. No model output reaches the draft un-checked. - Not-legal-advice banner. Prepended to every summary by the summary writer; no config flag disables it. The Sonnet prompt is instructed to describe and explain, never to recommend; a lint pass rewrites or drops any sentence that drifts into prescriptive “you should” phrasing.
- Human-in-the-loop. A summary is never
approvedby the system. High-stakes flags (rules-doc thresholds in/cs/config/flag-rules) sethigh_stakes=trueand route the draft toawaiting_humanuntil a person acts. The owner can also send any draft to a human on demand.
EventBridge Scheduler and SES
cs-drive-watch—rate(5 minutes). Target:drive-watchLambda.cs-monthly-summary—cron(0 9 ? * 2#1 *)(first Monday 9am) inTZ_NAME. Target:summary-monthlyLambda.- Set the MX record on a dedicated subdomain (e.g.
review.your-company.com) toinbound-smtp.ap-southeast-1.amazonaws.com. - SES inbound rule set
cs-inbound-rules: one rule with recipientreview@your-company.com→ spam scan → S3 PUT tos3://cs-raw-mime/<message-id>→ stop. The S3 PUT triggersintake-ses. - SES outbound for delivered summaries and lawyer hand-offs: verify a sender identity at
summaries@your-company.comwith DKIM and SPF. Out of sandbox by request.
IAM (least privilege per Lambda)
Each Lambda has its own role with policies scoped to exact ARNs. Sketch:
- reader role:
s3:GetObjectoncs-contracts;s3:PutObjectoncs-summaries;textract:StartDocumentTextDetection+StartDocumentAnalysis+GetDocument*;bedrock:InvokeModelon the Haiku, Sonnet, and Titan ARNs;s3vectors:PutVectors+QueryVectorsoncs-clauses;dynamodb:PutItemoncs-jobs;events:PutEventsfor the done event. - deliver role:
s3:GetObjectoncs-summaries;secretsmanager:GetSecretValueon the Slack and Drive secrets;ses:SendRawEmail; outbound network toslack.comandwww.googleapis.com. - approve-handler role:
dynamodb:PutItemoncs-jobsandcs-audit;s3:GetObject/PutObjectoncs-summaries;ses:SendRawEmailfor the human hand-off;secretsmanager:GetSecretValueon the Slack signing secret. - intake-ses role:
s3:GetObjectoncs-raw-mime;s3:PutObjectoncs-contracts. - drive-watch and esign-webhook roles:
secretsmanager:GetSecretValueon the relevant secret;s3:PutObjectoncs-contracts;dynamodb:PutItemoncs-jobs; outbound network to the Google or e-sign API host.
Review and approval flow
Summaries delivered to Slack use chat.postMessage with Block Kit blocks carrying the three action buttons; clicks hit the configured Interactivity request URL, which is the approve-handler Function URL. The handler verifies the Slack signing secret, parses the action_id (approve, send_to_human, ack_hold), and processes it. Email-delivered summaries carry signed links to the same handler. The Slack app needs chat:write and the Interactivity URL configured; the bot token lives in Secrets Manager under cs/slack/bot-token, the signing secret under cs/slack/signing-secret.
Observability and cost gates
- CloudWatch Logs: all Lambdas, 7-day retention, structured JSON. Subscription filter on
"error"+"throttle"+"timeout"to a metric for alerting. - Alarms:
readerfailures > 0 in an hour (the read is the one piece that has to work); Textract job failures > 1%; approve-handler signature-verification failures > 5/hour (a rotated Slack secret). - 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
cs-cost-alarmsubscribed to the on-call admin’s email and Slack.
Config and secrets
Drive service-account credentials live in Secrets Manager under cs/drive/sa; the e-sign HMAC secret under cs/esign/hmac; Slack bot token and signing secret under cs/slack/*. The always-flag list, the escalate-to-a-lawyer thresholds, the summary layout, the banner wording, the delivery surface, and the lawyer/manager hand-off address all live in Parameter Store under /cs/config/ (mirrored from the house-style Drive docs by a small sync, so a non-engineer can edit a flag rule without a deploy). Lambdas fetch config on cold start and cache for the execution environment’s lifetime.
Deploy
GitHub Actions with OIDC into a deploy role (no long-lived keys), building and shipping with AWS SAM. The opinionated bits: deploy the SES rule set as a separate stack (rule-set changes affect mail flow), turn on S3 versioning for cs-contracts and cs-summaries so any version of a contract or summary can be pulled up later, and keep the two Function URLs (esign-webhook, approve-handler) signature-verified rather than public-open. Total deployable surface: around seven Lambdas, two DynamoDB tables, three S3 buckets, one S3 Vectors index, one EventBridge rule on the default bus (plus the Scheduler rules), one SES rule set, 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