A form intake router on AWS for a few dollars a month
A small business website usually has more forms than anyone keeps track of. The contact form that emails a shared inbox nobody checks on weekends. The quote-request form wired to a plugin that broke after an update and silently stopped sending. The booking form that lands in someone’s spam folder. The newsletter signup that goes nowhere. Each one is a way a customer is trying to reach you — and each one is a place a lead can quietly disappear. This post walks through the design of a small router that catches every submission, checks it, files it safely, sends it to the right place, and always tells the customer “we got it.”
Key takeaways
- Every form on your site posts to one door, so no submission slips through an old plugin or a dead inbox.
- Every submission is saved and acknowledged before any slow downstream work, so the customer always gets a reply.
- A routing table decides where each one goes: a team email, a CRM list or sheet tab, and an auto-reply.
- Deliveries retry on a queue; if a tool stays down, the submission waits safely and is never dropped.
- Designed on AWS for about $2/month at typical small-business volume.
The whole system on one page
Before any code, here’s the shape of what we’re designing.
What you set up once (the outside)
- Website forms. Every form on your site — the contact form, the quote-request form, the booking form, the newsletter signup — gets a tiny snippet that posts the submission to one address (a Lambda Function URL, which is just a plain web address AWS gives a small function). You don’t replace your form builder or your site; you change where the form sends its data. Each form carries a short
form_idso the router knows which form it came from. Part 2 covers the snippet in detail. - A rules folder. Two short things in a Google Drive folder. The routing table is a Google Sheet with one row per form: which form, which team email should get it, which CRM list or sheet tab the row goes to, and which auto-reply template the customer gets. The rules and replies doc holds the actual auto-reply message templates (one per form) and the spam rules — the banned-word list, the rate limit, the honeypot field name. A non-technical owner can change where a form goes, or reword a reply, by editing the sheet. No deploy.
- Destinations. The places a checked submission ends up. The right team’s email inbox (sales, support, bookings). Your CRM or a Google Sheet that your team already lives in. And the customer’s own inbox, which gets a short “we got your message, here’s what happens next” reply so they’re never left wondering whether the form worked.
What runs on every submission (the inside)
- The intake door. The Function URL receives the form post. The very first thing it does is save the raw submission to S3 and write a record to DynamoDB — before any checking, before any sending. Then it replies to the browser so the customer’s form shows “thanks, we got it” in well under a second. Saving first means that even if every later step failed, the submission still exists and can be replayed. Nothing a customer typed is ever lost to a crash. Part 2 walks through this.
- The checker. Reads the saved submission. Checks the required fields are present and look right (an email that looks like an email, a phone that looks like a phone). Runs the spam rules: a hidden honeypot field a real person never fills in, a minimum time-on-page, a rate limit per address, and a banned-pattern list. Anything that looks borderline gets one cheap Bedrock Haiku 4.5 second-opinion rather than a guess. Then it looks up the routing rule for that
form_id. The checker is plain Python; the model is the exception, not the rule. Part 3 covers it. - Dispatch. Takes the routing decision and does the deliveries — email the team, write to the CRM, add the row to the sheet — but it does each one as a separate job on an SQS queue (a queue is just a waiting line for jobs). If a delivery fails because a tool is briefly down, the queue retries it a few times with growing gaps. If it still won’t go through, the job waits safely in a dead-letter queue (a holding line for jobs that keep failing) and an alert goes out, so a human can replay it once the tool is back. The customer reply and the team email are separate jobs, so a slow CRM never delays the “we got it.” Parts 4 and 5 cover this.
In plain words
A prospect fills in your quote-request form at 11pm on a Sunday: name, email, “need 200 branded mugs by month-end, what’s the price?” The intake door saves it and replies in their browser instantly: “Thanks — we’ve got your request and someone from sales will reply within one business day.” They also get that same line by email a moment later. The checker confirms the email is real, sees it’s not spam, and reads the routing table: quote requests go to sales@ and to the “Quotes” tab of your CRM. Dispatch emails the sales team the full submission and writes the CRM row. Your CRM happens to be mid-maintenance for ten minutes — the write fails, the queue waits, retries, and succeeds on the third try. Monday morning, sales sees the lead in both their inbox and the CRM, and the customer already feels looked after. Nobody had to notice anything went wrong.
The cost of running this is about $2 a month at SMB volume. The cost of not running it is the one quote request that vanished into a broken plugin, the booking that went to spam, or the contact form that a customer filled in twice because the first time it looked like nothing happened.
Design rules that shaped every decision
- Save and acknowledge first. The submission exists and the customer is told before any downstream work runs.
- Never drop a lead. Every delivery retries; anything that keeps failing waits in a dead-letter queue, never deleted.
- Favor the real lead over the blocked one. Spam-flagged submissions are held for review, not silently thrown away.
- One door, one routing table. Adding a form or changing where it goes is a sheet edit, not a deploy.
- Plain Python on the hot path. A model is called only when a check is genuinely borderline.
- Every submission is logged. You can answer “what happened to that lead?” for any submission, any day.
Why this shape
Most small businesses wire forms one of three ways: a form-builder plugin that emails a shared inbox, a third-party form service that charges per submission and locks your leads in their dashboard, or a custom script that worked the day it was written. All three share the same weakness — when the delivery step fails, the customer’s submission is just gone, and usually nobody finds out until a prospect calls asking why they never heard back. The plugin update that broke the email. The service that hit its free-tier cap. The script that timed out because the CRM was slow.
The setup above fixes the weakness by separating “we received it” from “we delivered it.” The moment a form posts, the submission is saved and the customer is told — that part can’t fail quietly because it happens first and depends on nothing else. Delivery to your team and your tools happens afterward, on a queue that retries and never deletes. The router is invisible most days; you only notice it on the day a tool would otherwise have eaten a lead, and on that day it simply waits and tries again.
The next four posts walk through each piece in turn: how a form submission gets captured, how it gets checked, how it finds the right tool, and how it gets confirmed and never dropped. One diagram per post. A cost breakdown and a final engineering reference at the end.
All posts