Somewhere out there is a person who, on a Thursday evening earlier this year, decided they fancied spending their evenings in the woods near Saffron Walden eating pudding and learning to be a grown-up around a fire. They found a little page I had built, filled in their details, and pressed the button. And then, on my side of the wire, absolutely nothing happened.
No email landed in my inbox. No cheerful little "you have a new sign-up" notification. As far as the outside world was concerned, the page had swallowed them whole. I'll be honest with you: I only found out because I went poking around in the data days later, half expecting it to be empty, and there they were. Sitting safely. Waiting. An enthusiastic enquiry that nobody had told me about.
What I was building
The page was for Nature of Learning, our family forest school. They wanted to run an adult evening session called Pudding Club — Thursday evenings, near Saffron Walden, the sort of thing where you don't need to be a child to deserve an evening outdoors. They needed somewhere for interested adults to register their interest. A single registration-of-interest page. Capture a name, capture some details, let someone know a human had put their hand up.
This is squarely in my comfort zone. ASP.NET, C#, a Blazor page. I have built a thousand forms in my career and I will build a thousand more. The actual mechanics of "take input, store it, notify someone" are not where the interesting problems live. Which, with hindsight, is exactly why I got careless.
The bit I actually cared about was making it not look like a developer had built it on a Tuesday. Sarah's printed leaflet has a real personality to it, proper woodland warmth, and I did not want a sterile Bootstrap form bolted onto that. So I pulled the actual fonts off the printed leaflet, gave the whole thing a proper woodland aesthetic, and fussed over it until it looked like it belonged. I spent the effort where it would be seen and skipped the effort where it wouldn't. Remember that, it matters later.
The plumbing I reused
For the notification email I did the sensible, lazy, correct thing: I reused an AWS SES integration I had already written for another one of my own codebases. No point writing email-sending from scratch when you have a working version sitting in a project down the road. Lift it, drop it in, point it at the right place, done.
The integration was config-driven, the way these things should be. The code reads its settings — the AWS keys and region, the usual Aws__ values — out of configuration at runtime. Clean. Sensible. The sort of thing you would happily show someone in a code review. AI helped me wire it all up and it was, genuinely, good code. It read the right keys, in the right order, with sensible fallbacks. There was nothing wrong with it at all.
And it worked in dev. Of course it worked in dev. In dev the settings were right there, where I could see them, where everything is forgiving and local and on my machine. I tested it, the email arrived, I felt clever, I shipped it. Everything that works on your machine is telling you the truth about your machine and nothing about production.
The post-mortem, in two sentences
When the sign-up came in and no email arrived, the whole investigation took about as long as one exchange of messages. I will reproduce it more or less verbatim, because it is the most honest thing in this entire post:
"Did you ever add those two Aws__... settings in the Azure portal?"
"No — I thought you added them in the app settings!"
"That'll be why then."
That is it. That is the whole bug. The code that reads the configuration values was perfect. The configuration values themselves were never put into the Azure portal. The application asked for a secret that nobody had ever given it, shrugged, and failed to send an email into the void. No exception loud enough to wake me. No crash. Just a quiet absence where a notification should have been.
Here's the thing though — the lead was never lost
This is the part I want to dwell on, because it is the difference between an embarrassing story and a disaster. The sign-up was captured. The data write happened first, completely independently, and it succeeded. The person's interest was sitting safe in the database the entire time. The only thing that broke was the bit that tells me about it.
That was not luck. Somewhere along the way I had built it so the page captures the data first and sends the notification second. Two separate steps. So when the email step fell over because of a missing key, it cost me a notification — not a person. I'd love to claim I designed that defensively because I'm wise. Mostly I think I got it right by habit and then got reminded, hard, why the habit is worth keeping. Capture the data first and notify second, so a missing secret costs you an email and not a lead.
Reverse those two steps — fire the email inside the same path that stores the record, let a failure there take the whole thing down — and that person doesn't end up sitting safely in the data. They end up nowhere. They fill in the form, see a spinner or an error, assume the woodland people aren't interested, and go and do something else with their Thursdays. Nobody writes about this part, because when you get it right nothing happens, and "nothing happened" makes a poor war story.
The handoff gap
Here is what I actually took away from it, and it is not really about AWS or Azure or email at all.
The AI wrote the code that reads the secret. It cannot — and absolutely should not — be the thing that types my production secrets into the Azure portal. That is a human step, on purpose. Which means there is a seam, a join, a handoff: the code says "I will need these two values at runtime" and a human has to go and physically provide them in a place the code never sees during development. The boundary between what the AI does and what the human does is exactly where things fail silently.
And they fail silently precisely because both sides did their job. The AI did its half perfectly. The code is correct. There is no bug to find, no red squiggle, no failing test, because the code is not wrong. The thing that's wrong is on the other side of the handoff, in the bit of the world that lives outside the repository, in a portal, behind a login, in a step somebody assumed somebody else had done. "I thought you'd done it" is not a coding error. It is an ownership error. And ownership errors don't show up in a diff.
I think this gets more dangerous as the AI gets better, not less. When the code it produces is genuinely good, you stop scrutinising the seams. You read the integration, you think "yes, that's how I'd have written it," and you tick it off. The very competence of the generated code lulls you past the question that actually mattered: and who is putting the secret in production? The model can't answer that. It doesn't know what's in my Azure portal. It can only tell me what the code will go looking for.
What I do differently now
A few practical things, all cheap, all things I should have been doing already.
First, the data-first ordering is now a rule I say out loud rather than a habit I rely on. Capture the thing that matters, commit it, then attempt the nice-to-haves like notifications. If the notification fails, log it loudly and move on, but never let it take the actual record down with it.
Second, I treat every config-driven integration as carrying an invisible to-do item that lives outside the code: "human must add these keys, here, in production." When the AI generates code that reads Aws__AccessKeyId from configuration, the honest output is not just the code — it's the code plus a sentence saying "you now need to put this value in the Azure portal, and dev passing proves nothing about whether you have." I now ask for that sentence explicitly. The model is happy to give it. I just have to remember to want it.
Third, and this is the soft one — be explicit about who owns each side of the handoff. "I thought you added them" only happens when nobody said whose job it was. With a human collaborator that means saying it. With AI it means knowing that the model owns the code and I own the world the code runs in, every single time, no exceptions, even when the code is so good it feels like the world should sort itself out.
The page is fine now. The settings are where they should be, the emails arrive, Pudding Club has its sign-ups and our enthusiastic early enquirer presumably got their reply in the end. But the thing I keep coming back to isn't the fix. It's how close I came to never knowing there was anything to fix. The code was perfect. The secret was missing. And the only reason it's a funny story instead of a sad one is that, by habit more than wisdom, I'd saved the person before I tried to email myself about them.