Part 7 of 7 · Vendor onboarder series ~8 min read

Engineering reference: the vendor onboarder architecture

Same system, drawn for engineers. Region, service names, resource identifiers, Bedrock model IDs, Lambda inventory, IAM scopes, the SES inbound rule set, EventBridge Scheduler config, the DynamoDB schemas, and the Function-URL upload and approve 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, Bedrock cross-Region inference, Textract, and EventBridge Scheduler are all available 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 a vendor onboarded a day late, not a regional outage. One AWS account dedicated to the onboarder (separate from your other workloads) keeps the IAM blast radius small and lets a single AWS Budgets alarm cover the whole system.

Topology

AWS topology of the vendor onboarder A topology diagram with three regions stacked vertically inside one AWS account boundary. Top region: intake. Three boxes show the three start lanes — a web-form Function URL Lambda named intake-form that creates the vendor file, an SES inbound rule set with action S3 PUT to s3://vo-raw-mime/ plus the intake-ses-parser Lambda that calls Bedrock Haiku 4.5 to read the supplier name and contact and propose a vendor, and a drive-sync Lambda triggered hourly by EventBridge Scheduler that reads new rows from the Google Sheet and starts a vendor each. Middle region: collect and check. The checker Lambda is triggered by an S3 PUT when a vendor uploads a document to s3://vo-documents/; it runs Textract, calls Bedrock Haiku 4.5 to read the document type and fields, decides present and in-date with plain Python, and updates the vendor's checklist row in DynamoDB vo-vendors; the chase Lambda is triggered daily at 9am local by EventBridge Scheduler, reads vo-vendors, and for each collecting vendor picks one of four moves, sending a reminder via SES outbound and logging to vo-chase. Bottom region: approve and record. When a file is complete the chase Lambda emails the owner a link to the approve page, a Function URL Lambda named approve-handler; on Approve it writes the vendor to the system of record via its API, locks the S3 folder, and writes vo-audit; Request-fix re-opens one checklist item; Reject closes the file. CloudWatch Logs collects from every Lambda at 7-day retention. Across the right edge: a small box labelled AWS Budgets alarm at $20 monthly threshold, posting to SNS topic vo-cost-alarm. A note at the bottom: nothing is added without a human Approve — and every interaction is logged to vo-audit. Intake Lambda · intake-form Function URL name, email, type → s3://vo-documents/ + vo-vendors row SES inbound rule set vo-inbound-rules action: S3 PUT s3://vo-raw-mime/ trigger: intake-ses-parser Lambda · drive-sync hourly poll Sheets API for new vendor rows → start vendor Vendor file started S3 folder + vo-vendors row + link Collect & check Lambda · checker S3 PUT on upload Textract + Haiku 4.5 present? in date? updates vo-vendors EventBridge Scheduler cron(0 9 * * ? *) in TZ_NAME target: chase Lambda + deferred one-offs Lambda · chase reads vo-vendors picks one of four moves SES reminder, only missing items → vo-chase Approve & record File ready → owner chase emails the owner a link to the approve page when every item is done Approve page [Approve] [Request fix] [Reject] button clicks → Function URL Lambda · approve-handler on approve writes the vendor to the system of record, locks S3, writes vo-audit Nothing is added without a human Approve — and every interaction is logged to vo-audit.
Fig 7. AWS topology, in three regions of the diagram: intake (three lanes start a vendor file), collect and check (uploads read on arrival, a daily chase for the rest), approve and record (the owner signs off and the vendor is written to the system of record). Every Lambda is event- or schedule-driven; nothing is synchronous-chained.

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.

  • intake-form — Lambda Function URL, public with AuthType: NONE. Serves the owner-facing start form (GET) and handles the submit (POST). On submit, creates the vendor folder under s3://vo-documents/<vendor-id>/, writes a vo-vendors row seeded from the checklist doc for the chosen type, and triggers the invite email via SES. The upload page for the vendor is served by the same function under a token-scoped path. Memory: 256 MB. Timeout: 15 s.
  • intake-ses-parser — S3 PUT trigger on s3://vo-raw-mime/. Parses the forwarded MIME, calls Bedrock Haiku 4.5 (anthropic.claude-haiku-4-5-20251001-v1:0 via global.anthropic.claude-haiku-4-5-20251001-v1:0) to read the supplier name and contact, and posts an owner confirmation message with a one-tap vendor-type choice. On confirm, starts the vendor the same way intake-form does. Memory: 256 MB. Timeout: 30 s.
  • drive-sync — EventBridge Scheduler target, hourly. Uses the Google Sheets API (service-account credentials in Secrets Manager under vo/drive/sa) to read new vendor rows from the start sheet and start a vendor for each. Also writes each vendor’s checklist status back to the sheet for the owner-facing view. Memory: 256 MB. Timeout: 30 s.
  • checker — S3 PUT trigger on s3://vo-documents/. Runs Textract via StartDocumentTextDetection + StartDocumentAnalysis (asynchronously to handle multi-page documents). On Textract completion (via SNS notification), calls Bedrock Haiku 4.5 to read the document type and fields, then plain Python decides present-and-in-date against the checklist rules. Posts a one-tap confirmation to the owner; on confirm, marks the item done in vo-vendors. For DOCX uploads (Textract doesn’t accept them), falls back to python-docx; XLSX uses openpyxl. Both packages are stable and widely used in 2026 though lightly maintained — acceptable for a path that runs a few times per vendor; if precision becomes a concern, the active fork python-docx-oss is a drop-in alternative. Memory: 512 MB. Timeout: 60 s.
  • chase — EventBridge Scheduler target, daily at 9am local time (the schedule expression runs in TZ_NAME set to the SMB’s timezone, e.g. Asia/Singapore). Reads vo-vendors, and for each vendor in the collecting state computes days-since-invite and the outstanding items, picks one of four moves (done/nudge/follow-up/escalate), and sends a reminder via SES SendRawEmail listing only the missing items. On quiet-hours defer, creates a one-off EventBridge Scheduler rule. Writes a row to vo-chase after each send. When a vendor reaches done, emails the owner the approve-page link. Memory: 512 MB. Timeout: 60 s. No Bedrock calls.
  • approve-handler — Lambda Function URL, public with AuthType: NONE; verifies a signed token on the request. Serves the owner’s approve page (GET) and handles Approve/Request-fix/Reject (POST). On approve, writes the vendor to the system of record via its API, sets an S3 Object Lock-style write-protection on the vendor prefix, marks the vendor approved, and writes vo-audit. On request-fix, re-opens one checklist item and re-arms the chase for it. On reject, closes the file with a reason. Memory: 256 MB. Timeout: 15 s.
  • digest — EventBridge Scheduler target, weekly Monday 9am. Reads vo-vendors and vo-chase; sends the owner a summary of vendors collecting, ready-to-approve, and approved that week. No Bedrock; a plain summary table. Memory: 256 MB.

Storage

  • DynamoDB · vo-vendors — one row per vendor. PK vendor_id; attributes: name, contact_email, type, internal_owner, state (collecting/ready/approved/rejected), invited_at, and a checklist map of item → {status, present, in_date, expiry, confirmed_by, confirmed_at}. On-demand.
  • DynamoDB · vo-chase — one row per reminder sent. PK (vendor_id, sent_date); attributes: move (nudge/follow-up/escalate), recipient, missing_items. On-demand.
  • DynamoDB · vo-audit — one row per write action of any kind. PK (vendor_id, ts); attributes: action (approved/request_fix/rejected/item_confirmed), by_user, snapshot. On-demand. No TTL — this is the long-term audit trail.
  • S3 · vo-documents — one prefix per vendor holding the uploaded documents. Versioning enabled; write-protection applied to a vendor prefix on approval. Lifecycle to Glacier at 90 days; expiry at 7 years.
  • S3 · vo-rules-source — mirrored checklist and voice docs as plain text. Versioning enabled.
  • S3 · vo-raw-mime — raw inbound MIME from forwarded supplier emails. Lifecycle to Glacier at 30 days; expiry at 7 years.

Bedrock and Textract

  • Foundation model. anthropic.claude-haiku-4-5-20251001-v1:0 via the Global cross-Region inference profile global.anthropic.claude-haiku-4-5-20251001-v1:0. Two callsites: checker for reading uploaded documents, and intake-ses-parser for reading the supplier name and contact off a forwarded email. A heavier summary-style narrative isn’t needed here; if you later add a quarterly vendor-base report, route that one call to anthropic.claude-sonnet-4-6-20250930-v1:0 for the longer reasoning.
  • Textract. StartDocumentTextDetection for plain document text and StartDocumentAnalysis with the FORMS feature for key-value pairs (tax-ID, account number, expiry date). Async, with completion via SNS to the checker Lambda.
  • Embeddings. Not used. The checklist is structured rows; deterministic lookup beats vector retrieval here. No Knowledge Base, no S3 Vectors. (If you ever add “find similar past vendors,” that’s when Amazon Titan Text Embeddings V2 at 1024-dim plus S3 Vectors would earn its place — not before.)

EventBridge Scheduler config

  • vo-daily-chasecron(0 9 * * ? *) in the SMB’s timezone. Target: chase Lambda.
  • vo-drive-syncrate(1 hour). Target: drive-sync Lambda.
  • vo-weekly-digestcron(0 9 ? * MON *) in TZ. Target: digest Lambda.
  • One-off rules — created on the fly by chase when a quiet-hours defer is needed. Use at(YYYY-MM-DDTHH:MM:SS) expressions with --action-after-completion DELETE so the rule self-cleans.

SES inbound and outbound

  • Set the MX record on a dedicated subdomain (e.g. vendors.your-company.com) to inbound-smtp.ap-southeast-1.amazonaws.com.
  • SES inbound rule set vo-inbound-rules: one rule with recipient vendors@your-company.com → spam scan → S3 PUT to s3://vo-raw-mime/<message-id> → stop. The S3 PUT triggers intake-ses-parser.
  • SES outbound for invites, reminders, and the approve-page email: verify a sender identity at onboarding@your-company.com with 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:

  • chase role: dynamodb:Query + GetItem on vo-vendors; dynamodb:PutItem on vo-chase; ses:SendRawEmail from the verified sender identity; scheduler:CreateSchedule for the deferred-send one-offs. No bedrock:*.
  • checker role: s3:GetObject on vo-documents; textract:StartDocumentTextDetection + StartDocumentAnalysis + GetDocument*; bedrock:InvokeModel on the Haiku ARN; dynamodb:UpdateItem on vo-vendors; dynamodb:PutItem on vo-audit.
  • approve-handler role: dynamodb:UpdateItem on vo-vendors; dynamodb:PutItem on vo-audit; secretsmanager:GetSecretValue on the accounting-tool API secret; s3:PutObjectRetention on the vendor prefix; outbound network access to the accounting tool’s API host.
  • intake-form and intake-ses-parser roles: s3:PutObject on vo-documents and read on vo-raw-mime; dynamodb:PutItem on vo-vendors; ses:SendRawEmail; bedrock:InvokeModel (parser only).
  • drive-sync role: secretsmanager:GetSecretValue on the Google service-account secret; dynamodb:Query + PutItem on vo-vendors; outbound network to sheets.googleapis.com.

Upload and approve flow

Both vendor-facing and owner-facing surfaces are Lambda Function URLs, never API Gateway. The vendor’s upload page is served by intake-form under a path carrying a long random per-vendor token (stored on the vo-vendors row); the token scopes the page to one vendor so nobody can reach another’s documents. Uploads use a short-lived S3 presigned PUT URL minted by the Function URL, so the file goes straight to vo-documents and the S3 PUT triggers checker. The owner’s approve page is served by approve-handler under a separate signed token with a short TTL; the Approve/Request-fix/Reject actions POST back to the same function, which verifies the token before doing anything. There is no account and no password on either side.

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: chase Lambda failures > 0 in a day (the daily tick has to run); checker failure rate > 1% in 24h; approve-handler token-verification failures > 5/hour (might mean a leaked or stale link).
  • X-Ray: off by default. Not worth the cost at SMB volume.
  • AWS Budgets: $20/month threshold, alarm at 80% and 100%, posts to SNS topic vo-cost-alarm subscribed to the on-call admin’s email.

Config and secrets

Service-account credentials for the Sheets API live in Secrets Manager under vo/drive/sa. The accounting-tool API key (for writing approved vendors to the system of record) lives under vo/system-of-record/api. The configured timezone, quiet-hours window, chase cadence defaults, and the per-type checklist reference all live in Parameter Store under /vo/config/. Lambdas fetch config on cold start and cache for the lifetime of the execution environment.

Deploy

GitHub Actions with OIDC into a deploy role (no long-lived keys) and 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 vo-documents and vo-rules-source so a bad upload or doc edit can be rolled back, and version the EventBridge Scheduler timezone setting so you don’t accidentally start running the daily chase in UTC after a CI rotation. Total deployable surface: around seven Lambdas, three DDB tables, three S3 buckets, 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