This describes where the sending infrastructure goes as volume grows past one shared inbox — more Workspace inboxes, more sending domains, real deliverability monitoring, branded links. The pipeline itself (Lambda/DynamoDB/EventBridge, Bedrock, the two human-review gates) doesn't change; see architecture.html for that, still the as-built system. This is a roadmap, not a deploy.
MailReach (Option C) is connected — wifiscanner@trywifiscanner.com has warmup running, and its score is pulled live into admin-status.ts/admin.html. Smartlead (Option B) was evaluated and not pursued.
Clay sourcing no longer needs the manual "build a table, wire an HTTP API action" loop this doc assumed — clay-source.ts calls Clay's Search + Routines API directly, scheduled per approved campaign. See the repo README, "Clay lead sourcing."
Domain/inbox provisioning — the "manual-ish loop" described below now has a real automated version, just via a bigger mechanism than this doc originally scoped: a parallel ops platform (separate CDK app, Cognito-authenticated Fargate dashboard, Step Functions workflow) that registers DNS, adds a domain to Workspace, creates the inbox, and marks it ready — all with zero code change to send-scheduler.ts/reply-poller.ts. Real domain purchasing is deliberately not implemented there either (needs real WHOIS contact data); it always assumes the domain is already registered by hand.
1.0 proved the pipeline end to end on one shared Workspace inbox. 2.0 is entirely about what feeds that pipeline's inbox-pool table and gmail-oauth secret — more sending identities, spread across more domains, with real visibility into whether any of them are burning reputation. Nothing about ICP discovery, Clay sourcing, personalization, or the approval gates changes; send-scheduler.ts already round-robins across whatever's in the inbox pool, so this is additive capacity, not a rebuild.
30 inboxes × 10 sends/day = 300/day baseline. That's a useful checkpoint to be explicit about: this is well below the 1,000–5,000/day range where dedicated cold-email infra operations live, and most of what makes their tooling complex (bulk inbox marketplaces, per-alias throttling consoles, managed warmup-as-a-service) is solving a problem this scale doesn't have yet. The operating principles those operations use — don't run inboxes at the edge of their limit, isolate domains so one bad one doesn't take the rest down, watch signal and rotate out underperformers rather than trying to "fix" a domain — hold at any scale, and are what 2.0 actually borrows.
| Quantity | 1.0 (today) | 2.0 target |
|---|---|---|
| Sending inboxes | 1 (wifiscanner@trywifiscanner.com) | ~30, added via the admin form's bulk-paste |
| Sending domains | 1 (trywifiscanner.com) | Several — see domain strategy below |
| Per-inbox daily cap | 10 | 10 (unchanged — cap stays conservative regardless of pool size) |
| Effective daily volume | ≤10 | ≤300 |
Two ways to add sending capacity, with different isolation properties:
Each gets its own SPF and DKIM selector, so reputation is mostly independent. Cheapest to set up — one Route 53 zone, no new domain purchases. Risk: if the apex domain itself gets blocklisted by a provider, subdomains under it can get caught in the blast radius, and DMARC policy inherits from the root unless a subdomain sets its own.
Full isolation — a burned domain only takes its own inboxes down. This is the actual firewall real cold-email operations rely on, treating each domain as a disposable unit rather than something to nurse back to health. Costs more (domain registration per unit) and more DNS setup, but at 30 inboxes split across, say, 5–6 domains (5-6 inboxes each), the blast radius of losing one domain is small.
Recommendation for this scale: a handful of separate root domains (not 30 — one per 5-6 inboxes), each registered through Route 53 like trywifiscanner.com was, each with its own SES-independent SPF/DKIM/DMARC (cold sends don't go through SES at all, so this is just Workspace + Route 53 DNS, no SES identity needed per sending domain). Reserve the actual product domain (wifiscanner.com) for the real site only — never for sending, same as today.
No third-party inbox marketplace (Hypertide-style vendors sell pre-warmed inboxes in bulk) — staying AWS + Google Workspace only means this stays a manual-ish loop per domain:
This is the one place a pure AWS+Google stack doesn't have a clean equivalent: dedicated cold-email platforms sell bulk inbox/domain provisioning and warmup-as-a-service specifically to remove this manual loop. At 30 inboxes it's tractable by hand (or scriptable against the Workspace Admin SDK later if it becomes tedious); it would not be at 1,000+. Worth naming now rather than discovering it as a surprise later if volume targets grow past what this doc sizes for.
Researched as an alternative to the manual provisioning loop above — API-only, no one operating Smartlead's own dashboard. Smartlead doesn't replace the pipeline's brains (Bedrock, the two approval gates, DynamoDB state stay exactly where they are); it would replace the mechanical send/receive layer mailer.ts currently owns, in exchange for managed inbox warmup and a reputation dashboard — the two things flagged as an honest gap in the sections above.
| Current piece | Smartlead API equivalent | Confidence |
|---|---|---|
| gmail-oauth secret + inbox-pool table | POST /email-accounts/save — takes the same raw App Password + smtp.gmail.com/imap.gmail.com already generated. Admin form's bulk-paste could push here instead of (or in addition to) Secrets Manager. | Confirmed |
| send-scheduler.ts (nodemailer SMTP send) | Bedrock's subject/opener pushed as a lead custom_field via POST /campaigns/{id}/leads; the campaign's one-step sequence template is just {{custom_field}} — fully custom per-lead content, indirected through a "sequence" wrapper | Confirmed |
| reply-poller.ts (IMAP poll, 5 min) | EMAIL_REPLY webhook — push-based, real-time instead of polled | Confirmed |
| reply-review-response.ts send, after human approval | POST /campaigns/{id}/reply-email-thread | Needs a lookup step — see below |
| unsubscribe.ts / suppression table | Kept as-is — check our own suppression table before ever adding a lead to a Smartlead campaign, rather than relying on Smartlead's own block-list | No change needed |
Framing: because it's strictly API-driven with no manual dashboard operation, it fits the "everything through the Lambda pipeline / admin form" philosophy even though it isn't AWS-only — think of it as AWS + Google Workspace + Smartlead-as-a-managed-API, not a platform switch. Worth a real trial-account spike (14-day free trial, 2,500 email credits) to confirm the two unconfirmed items above before deciding between this and the manual-provisioning path.
The smallest-footprint option of the three. Unlike Smartlead, MailReach doesn't want to own sending or replies — it's a reputation-building layer that rides alongside inboxes already in the inbox-pool table. Nothing in the existing pipeline changes: mailer.ts, send-scheduler.ts, reply-poller.ts, personalization, both approval gates, and the DynamoDB schema all stay exactly as built. The only additions are (1) registering each inbox with MailReach via API using the same App Password credentials already generated, and (2) pulling its per-inbox reputation score ("Heat Score") into admin-status.ts — directly filling the Postmaster-Tools-shaped gap flagged above, without OAuth.
| What | Detail |
|---|---|
| Connects via | IMAP/SMTP with the same App Password credentials, or OAuth — API access included on every plan, no premium gate (unlike Warmy, see below) |
| Network size | 30,000+ reciprocal inboxes |
| Reputation data | "Heat Score" per inbox, daily time-series, pullable via API; Slack/webhook alerts on drops |
| Pricing | $25/inbox/mo (1-5 inboxes), $19.50/inbox/mo (6-20 inboxes); pricing above 20 inboxes isn't published |
Recommendation: lowest-risk way to close the reputation-monitoring gap without touching a single line of the working send/reply pipeline — but it solves a narrower problem than Smartlead (reputation only, not real-time prospect replies), and needs a real 30-inbox quote before assuming it's the cheaper of the two paid options.
Clarifying which system sees what, since it's easy to check the wrong dashboard:
| Signal | Where it lives | Covers |
|---|---|---|
| SES reputation / bounce / complaint rate | SES console, admin-status.ts could surface it | Transactional mail only (ICP review, digest, meeting alerts) — not the cold sends, those go over Gmail SMTP |
| Spam rate, domain/IP reputation, auth pass rates | Google Postmaster Tools (per verified domain) | The actual cold-send signal. Requires a TXT-record domain verification per sending domain. |
| Reply rate, per-inbox / per-domain | Already computable from the leads table (status counts by sentFromInbox) | The most direct in-house signal — no new integration needed, just a query the status dashboard doesn't do yet |
| IMAP login health | admin-inboxes.ts test-connection (built, live today) | Catches a revoked App Password before it silently breaks sends |
Postmaster Tools has a JSON API, but it's OAuth-based — the same complexity tradeoff already declined once when this build moved from Gmail-API-OAuth to SMTP+App-Password. Recommendation: treat Postmaster Tools as a manual check-in per domain for now, and only build the API integration if reply-rate data (which is easy to automate) starts suggesting a specific domain has a problem worth digging into. (Smartlead's own reputation dashboard, or MailReach's Heat Score API, would each cover most of this table without a separate Postmaster integration — see Options B and C above.)
An audit of the current stack against what's actually deployed: Lambda, DynamoDB, API Gateway, EventBridge Scheduler, Secrets Manager, SES, Bedrock, S3, Route 53. Grouped by what each addition actually fixes, not just "nice to have."
Worth naming plainly: the Bedrock "use case details" access gate that silently broke sends earlier in this build's history went undetected until manual testing caught it. A CloudWatch Alarm on Lambda Errors would have caught it the moment it started failing.
Built — every core pipeline Lambda (all except the admin-* test-harness functions, whose errors already surface immediately in admin.html) now has a CloudWatch Alarm on its Errors metric (≥1 in a 5-minute window), feeding one SNS topic subscribed to SALES_EMAIL. Requires confirming the SNS subscription email once — see README.
| Resource | Fixes | Status |
|---|---|---|
| CloudWatch Alarms + SNS | Silent Lambda failures (the Bedrock-access incident, specifically) | Done |
| CloudWatch Dashboard | Free aggregate view — invocations/errors/duration per Lambda, DynamoDB consumed capacity/PutItem throttles, API Gateway count/4xx/5xx/latency — no new Lambda code, complements admin-status.ts rather than replacing it | Done |
| API Gateway access logging | Zero request-level logging at the API layer today — only whatever each Lambda logs individually | Done |
| X-Ray tracing | Active tracing on every core Lambda — each function's own execution segments (including its outbound AWS SDK/fetch calls) are now visible per-invocation. Doesn't by itself stitch one lead's full ICP→approve→Clay→send→reply chain into a single trace (that needs explicit trace-ID propagation through DynamoDB records — a bigger lift, not taken here), but is real value for "why was this one invocation slow," which is how the thread-drift/repetition/Bedrock-access bugs were actually diagnosed | Done (per-Lambda, not cross-chain) |
| Resource | Fixes | Status |
|---|---|---|
| DLQ (SQS) on the EventBridge Scheduler targets | All five CfnSchedule resources now set deadLetterConfig against one shared queue — if a scheduled invocation fails repeatedly, it lands there instead of just disappearing with no record | Done |
| DynamoDB Point-in-Time Recovery | Enabled on all four core tables (campaigns/leads/suppression/inbox-pool) — a bad write/delete can now be restored to any point in the last 35 days | Done |
| DynamoDB TTL | Enabled on leads only — a weekly leads-ttl-sweep.ts marks terminal-status rows (sent/replied/meeting_booked/suppressed/bounced/exhausted) older than 2 years for expiry. Deliberately not added to suppression — an unsubscribe/bounce record must never auto-expire, that risks re-emailing someone who opted out | Done |
| Resource | Fixes | Status |
|---|---|---|
| CloudFront + ACM in front of the docs S3 bucket | The site — including admin.html, which has the admin-token entry field — used to be served over plain HTTP with a publicly-readable S3 website endpoint. Now: a private (Origin Access Control-only) bucket behind CloudFront, HTTPS-only, custom domain (docs-development.trywifiscanner.com) | Done |
| WAF on the API Gateway | Turned out to need a real architecture change to work at all — WAFv2's WebACLAssociation doesn't support API Gateway HTTP API (v2), only REST API/ALB/AppSync/Cognito/etc (caught live: the direct attempt failed with an invalid-ARN error and rolled back cleanly). Fixed by putting CloudFront in front of the API instead — same move as the docs bucket and the ops platform's own dashboard — which doubles as the branded-URL goal below and gives WAF a resource type it actually supports | Done |
| Systems Manager Parameter Store | A /wifiscanner-sdr/<env>/sending-paused string parameter — flip to "true" and send-scheduler.ts skips every invocation on its next run, no redeploy needed | Done |
Every approval/unsubscribe/booking-adjacent link this pipeline emails now reads as go-development.trywifiscanner.com instead of a bare execute-api.amazonaws.com host — via the same CloudFront-in-front-of-the-API fix that made WAF possible above, not a direct API Gateway custom domain (which can't take WAF).
The pipeline's state machine stays implicit — DynamoDB status fields plus polling schedules. Asked directly and confirmed: this pipeline currently runs two real, approved campaigns with real sends in flight, and migrating every Lambda's trigger mechanism carries real risk to what's already live. The ops platform's own provisioning workflow already demonstrates Step Functions works well in this codebase when the blast radius is a new, isolated subsystem rather than the live send path itself.
Not taken — the existing DynamoDB Streams on leads/inbox-pool feed the ops platform's rollup sync, a different purpose than cutting personalize→send latency. Lower priority at 1.0's single-inbox scale; revisit once real volume approaches the 300/day target.
The ops platform's /provision/test page lets you pick a domain (or have Bedrock suggest a few) and click through a simulated provisioning run — registering → DNS → Workspace → inbox → ready — without any real side effect. Deliberately not a variant of the real Step Functions workflow: it never calls Route 53 or the Google Admin SDK, since a domain suggested or typed here was never verified to exist, let alone be owned. Everything it writes stays in the ops platform's own ops-domains/ops-audit-log tables, tagged source: "test-suggested" and shown with a (test) badge — never the pipeline's real inbox-pool table, which is the only one send-scheduler.ts/reply-poller.ts ever read. wifiscanner@trywifiscanner.com stays the only real sender regardless of what's provisioned here.
Unsubscribe and approval links currently point at the raw execute-api.us-east-1.amazonaws.com URL. API Gateway HTTP APIs support a custom domain name directly — an ACM certificate plus a Route 53 alias record pointed at the API's regional domain, no CloudFront required unless caching or a WAF is wanted in front of it later. A subdomain like links.trywifiscanner.com would cover unsubscribe, ICP-approval, and reply-approval links in one change to apiBaseUrl().
Sending capacity (300/day) and Clay's lead-sourcing plan tier are separate constraints — a bigger inbox pool doesn't source more leads by itself. Worth checking Clay's plan limits against the 300/day target independently once inbox scale-up is underway; if sourcing can't keep pace, the extra send capacity just sits idle.
| Phase | Scope | Status |
|---|---|---|
| 1 | Single shared inbox, test-harness form (admin.html), status dashboard | Done |
| 2 | Multi-inbox pool via bulk-paste (this doc's inbox management section) | Done |
| 3 | Additional sending domains, per-domain DNS — superseded by the ops platform's Step Functions provisioning workflow (bigger mechanism than originally scoped here, same outcome). Reply-rate-by-domain reporting: built as part of that platform's Phase D signal-evaluator, not this doc's original plan | Superseded — see ops platform |
| 3-B | Smartlead trial-account spike | Evaluated, not pursued |
| 3-C | MailReach — register inboxes, pull warmup score into admin-status.ts | Done (1 inbox connected via UI — no public API for mailbox creation, see README) |
| 4 | Branded links (CloudFront in front of the API Gateway — not a direct API Gateway custom domain, see Security & delivery above) | Done |
| 5 | Postmaster Tools API integration | Still skippable — MailReach's score already covers most of this gap |
| 6 | Reliability/observability/security sweep — DLQ, PITR, TTL, CloudWatch Dashboard, API Gateway access logs, X-Ray, WAF, SSM pause-toggle, CloudFront+ACM for docs | Done |
| 7 | Step Functions rearchitecture of the core pipeline's implicit state machine | Evaluated, explicitly declined — live campaigns in flight |
| 8 | Fictitious/test domain-picker preview (AI-suggested or manual domain names, simulated provisioning, zero real side effects) | Done |