Gmail → extracted quote in RDS (stages 1–4). Private posture: the whole pipeline runs inside a VPC with no public ingress — the only internet traffic is outbound to the Gmail API, via NAT. Notifications use a scheduled poll, not a push webhook. Yellow = AI.
flowchart TB
subgraph GC["☁️ Google Cloud — external"]
GM["Gmail · N inboxes
history.list + messages.get"]
end
subgraph VPC["🔒 AWS VPC — private · no public ingress"]
direction TB
subgraph PUBSN["Public subnet · egress-only"]
NAT["NAT Gateway"]
end
subgraph PRIV["Private subnets · multi-AZ · no public IPs"]
N1["λ Lambda · 1 Notification
scheduled poll · list since watermark"]
F2["λ Lambda · 2 Fetch
pull body + attachments"]
C3["λ Lambda · 3 Classify"]
X4["Fargate · 4 Extract
PyMuPDF + Tesseract"]
RDS[("RDS Postgres
private · SG-scoped")]
end
subgraph VPE["AWS services · via VPC Endpoints (PrivateLink) — no internet"]
SCH["EventBridge Scheduler"]
Q2(["SQS · to-fetch"])
Q3(["SQS · to-classify"])
Q4(["SQS · to-extract"])
HRQ(["SQS · human review"])
DLQ(["SQS · DLQ"])
TRK[("DynamoDB
watermark + status")]
S3[("S3
raw email + attachments")]
BR["Bedrock (Claude)
classify-fallback + extract"]
SEC["Secrets Manager"]
CW["CloudWatch
metrics · logs · alarms"]
SNS["SNS · alarm topic"]
end
end
subgraph OC["🔔 On-call — external"]
PD["PagerDuty"]
SLACK["Slack / ticket"]
end
SCH ==>|"every ~1–2 min"| N1
N1 -.->|"history.list · egress via NAT"| NAT -.-> GM
N1 <-->|advance watermark| TRK
N1 -->|new messageIds| Q2 --> F2
F2 -.->|"messages.get + attachments · egress via NAT"| NAT -.-> GM
F2 <-->|dedupe| TRK
F2 -->|store raw| S3
F2 -->|RawMessage refs| Q3 --> C3
C3 -.ambiguous.-> BR
C3 -->|mark classified/discarded| TRK
C3 -->|QuoteCandidate| Q4 --> X4
S3 -->|attachment bytes| X4
X4 <-->|structured extract| BR
X4 -->|ExtractedQuote P1| RDS
X4 -.low confidence.-> HRQ
Q4 -.poison.-> DLQ
SEC -.creds.-> N1 & F2 & X4
N1 & F2 & C3 & X4 -.metrics/logs.-> CW
DLQ -.depth alarm.-> CW
CW -->|alarms| SNS
SNS ==>|"critical → page"| PD
SNS -.->|warning| SLACK
RDS -.->|handoff → matching| NEXT["RFQ matching (out of scope)"]
classDef ai fill:#fde047,stroke:#ca8a04,color:#000
class BR ai
%% subgraph fills — distinct colours per zone
style GC fill:#f2f0ff,stroke:#8250df,stroke-width:1.5px,color:#1f2328
style VPC fill:#eef4ff,stroke:#2f6fd6,stroke-width:2px,color:#1f2328
style PUBSN fill:#e6fbf0,stroke:#1a7f37,stroke-width:1.5px,color:#1f2328
style PRIV fill:#e2ecff,stroke:#3b7ddd,stroke-width:1.5px,color:#1f2328
style VPE fill:#fff4e6,stroke:#e8992e,stroke-width:2px,color:#1f2328
style OC fill:#fdeef0,stroke:#d1242f,stroke-width:1.5px,color:#1f2328