On my own website, someone filled in the contact form and got a thank-you message. The lead reached nobody. No error, no alert, no queue to go digging through later — the submission was simply gone, and the person on the other end had every reason to believe I had it.
This is the first of three case studies I'm publishing as proof of work. It has no client in it, which is the point: Smart AI Workspace is client zero. I fixed my own lead intake before selling the same fix to anyone else, and you can check the result right now by submitting the contact form and watching the confirmation land in your inbox.
Client Zero: A Case Study With No Client
Most agency case studies are unverifiable by design. A logo, a percentage, an anonymised quote, and no way for you to check any of it. This one is the opposite: the system under discussion is the one you are reading this on, and the test of whether it works is a live form you can submit yourself in the next minute.
That constraint also shapes what I can claim. Everything below is either behaviour you can trigger yourself or a change with a commit behind it. Where I don't have a number, I don't give one.
The Bug: A Route That Returned Success With Nothing Captured
The original build routed POST /api/contact to an n8n webhook. The forward was wrapped in a conditional:
const webhookUrl = process.env.N8N_CONTACT_WEBHOOK_URL;
if (webhookUrl) {
const webhookRes = await fetch(webhookUrl, { /* ... */ });
if (!webhookRes.ok) {
console.error("n8n webhook error:", webhookRes.status);
return NextResponse.json({ error: "Failed to submit. Please try again." }, { status: 502 });
}
}
return NextResponse.json({ success: true });
Read the failure path. If N8N_CONTACT_WEBHOOK_URL was unset or pointed somewhere wrong, the whole block was skipped and the route still answered { success: true }. The visitor saw a thank-you. The lead reached nobody.
I wrote that conditional on purpose. It meant the form worked on my laptop without a webhook configured — a local development convenience. That convenience became a production hole, because the same code path that made development pleasant also made silent failure the default in the one environment where it mattered.
What It Cost
At least one real inbound lead was lost while the site ran that way. That is the entire claim, and I'm deliberately not dressing it up: no count, no deal value, no name. Someone wanted to talk to me, said so through the form, and I never saw it.
There is no getting that person back, and no way to know what else went the same way. A silent failure leaves nothing behind: no bounce, no error report, no row in a table to reconcile later. And the person who filled in the form has no reason to chase — as far as they know, they contacted you and you ignored them.
The Fix Was a Definition, Not a Vendor
The migration is one commit — feat(contact): route form submissions through Resend instead of n8n, 31 August 2026 — but swapping the destination is not what fixed anything. n8n was not the problem. The webhook did its job when it was pointed at a live workflow. The problem was my route's contract: it defined success as "I finished running" instead of "the lead is somewhere a human will see it."
The current route inverts that. It only answers { success: true } after the internal notification has actually been accepted for delivery. If that send returns an error, the route logs it and returns a 502, and the form tells the visitor to try again — which is the honest answer, because at that moment nothing has their message.
| Situation | Old route (n8n era) | Current route (Resend) |
|---|---|---|
| Destination not configured | Skipped the forward, returned success | Local-dev no-op only, when RESEND_API_KEY is absent |
| Notification send fails | 502 — but only if the webhook was configured | 502, logged, every time |
| Visitor sees a thank-you | Even when nothing was captured | Only after the lead is captured |
| Confirmation to the submitter | None | Sent, best-effort |
| Replying to a lead | Copy the address out of the workflow | Reply straight to the notification email |
One honest caveat, since the point of this post is that you can check my work: the local-dev escape hatch still exists in a narrower form. With no RESEND_API_KEY the route skips both sends and reports success, so nothing breaks while I develop offline. The difference is that a misconfigured destination can no longer pass as a success — with a key present, the only route to { success: true } runs through a send that actually worked.
Not sure your own intake is delivering?
Send me your form and I'll tell you what happens to a lead when your delivery destination is down. It takes one submission to find out.
Two Sends, Two Different Contracts
The rebuilt route sends two emails, and the distinction between them is the whole design:
- Internal notification to my inbox (
CONTACT_INBOX, a comma-separated list expanded into an array), withreplyToset to the submitter. This is the critical path. Its failure fails the request. - Customer confirmation to the submitter, with
replyToset back to my inbox. This is best-effort. Its failure is logged and nothing more, because by the time it runs the lead is already captured.
Getting that second contract wrong is the mistake I see most often in intake code someone has already tried to harden. A form that 502s a real prospect because a courtesy email bounced has traded one failure for a worse one. Rank your sends: everything before the capture is allowed to fail loudly, everything after it is allowed to fail quietly.
The Rest of the Hardening
Around that core, the same commit series added the boring parts that keep the critical path clean:
- Server-side validation — a service allowlist, length limits on every field, and an email format check, all enforced on the server rather than trusted from the client.
- Rate limiting — five requests per IP per hour, so a bot cannot bury a real enquiry under a thousand fake ones.
- Shared email primitives — one
emailShell()helper insrc/lib/email/shell.tsbuilds both emails, so the notification and the confirmation cannot drift apart. - A real sender address — mail goes out from a monitored mailbox,
tariq@, not anoreply@black hole. If someone replies to the confirmation, the reply reaches me. - Deliverability — SPF, DKIM and DMARC configured, because an email that lands in spam is the same silent failure wearing a different hat.
What Changed for Someone Who Contacts Me
Two things, and I'm keeping both modest, because they're the only before-and-after I can honestly state.
Enquiries used to get a reply the same day. Now they get one within the hour, because the notification arrives in a mailbox I actually watch, with the sender's address already in the reply field.
And the person who wrote in stops guessing. They get a branded confirmation that repeats their message back to them, from an address that accepts replies. That is the part I'd argue matters most: the gap between "I submitted a form" and "a human has this" is exactly where a prospect goes and contacts a competitor instead. Speed of first response is the cheapest advantage available to a small business, and I've written about why it's the first thing to automate in a lead-heavy industry.
Verify It Yourself
Go to the contact page, fill it in, and send. You should have a confirmation email shortly after, from a monitored address, with your own message quoted back to you. Reply to it and it reaches me.
That is the whole proof, and it is available without asking me for anything. If it ever stops working, you'll know before I do — which is a level of exposure I would rather have than a case study nobody can check.
Audit Your Own Intake
The transferable lesson here has nothing to do with Next.js, Resend or n8n. Any intake — a form, a booking widget, a chatbot handoff, a phone tree — should survive these five questions:
- What exactly does your success message promise? If it says "I'll be in touch," something has to guarantee that a human can be.
- What happens when the destination is down? If the answer is "the visitor still sees a thank-you," you have this bug.
- Which step is the capture? Everything before it must be allowed to fail loudly; everything after it must be allowed to fail quietly.
- Who reads the inbox it lands in? A shared alias nobody opens is the same as no delivery.
- When did you last submit your own form on production? Most silent failures are found by an owner testing their own site, or by a prospect who happened to follow up twice.
Run those five against your own intake this week. It costs one test submission.
How Smart AI Workspace Approaches This
I'm Tariq Osmani, founder of Smart AI Workspace. Every engagement is founder-led — the person who scopes your build is the person who writes the code, which is also why the first thing I automated was my own business.
That order is deliberate. I would rather find the silent-failure class of bug in my own lead flow, at my own cost, than discover it in yours. When I audit a client's intake, the five questions above are the ones I open with, and more often than not the answer to number two is the answer mine was. For the wider context on how this kind of work gets scoped and priced, see what an AI workflow automation consultant actually does and how I price.
Get Your Intake Audited
If you cannot say for certain what happens to a lead when your delivery destination fails, that is worth an hour of attention before it is worth a rebuild. Contact me for a free audit — I'll submit your form, trace where it goes, and tell you what breaks. See what I build, how I price, or check verified work history on my Upwork profile.
Case studies two and three are in progress, and they follow the same rule: nothing in them you cannot check.
Sources: Next.js — Route Handlers · Resend — Send Email API reference · RFC 7489 — Domain-based Message Authentication, Reporting and Conformance (DMARC) · Google — Email sender guidelines · MDN — Using the Fetch API
