Multi-Tenant Default Value Bug

My AI Wrote a Racing Sponsorship Pitch to a Marketing Consultant

AM
Allan Melo
August 17, 2026 · 6 min read

I promised in the last post that I'd write about what breaks, not just what works. This is the first payment on that.

Last week I was testing the free version of our tool. You give it your website and tell it who you sell to, it finds three real decision-makers in that market, and drafts the exact email it would send the first one. I asked it to sell ApexOutreach to a fractional CMO.

It found him. Correct market, real person, real address. Then it wrote him this:

For nearly 30 years, I put a childhood dream of becoming a racing driver on hold... my wife Marcela gifted me a track day at the BMW Performance Center... invited to join an endurance racing team for a 7-race campaign across iconic East Coast tracks for 2027...

That is not a hallucination. Hallucinations are vague. This has a named person, a named venue, a specific season and a specific race count, because it is real copy. It belongs to From Rig To Road, my racing sponsorship campaign, a different workspace in the same system. Marcela is my wife. The seven races are real.

So my product, asked to sell itself, pitched a marketing consultant on sponsoring a race car.

I made myself diagnose it before I fixed it

The instinct is to go straight to the code and stop the bleeding. I didn't, and I think that was the best decision in this story, because there were two possible causes and they are not the same kind of problem.

The first is that the drafting code read another tenant's data. One workspace's private copy reaching another's output is not a bug, it's a data breach, and it changes what you do next: you disclose, you audit what else that path can reach, you stop shipping until you know.

The second is that the copy arrived some other way. A cached prompt, a shared object between runs, a stale constant. Bad, but bounded.

You cannot tell these apart from the output, so I wrote the requirement down before touching anything: name the cause, with evidence, before proposing a remedy.

It was the second one, and the evidence was structural rather than a matter of reading carefully and feeling confident. The drafting path performs zero workspace reads. No query in it could return another tenant's data, which rules out a cross-tenant read by construction instead of by inspection. "I read it and it looks fine" is not a finding. "The code that would have to exist for this to be a breach does not exist" is.

The actual cause was a default

Back when From Rig To Road was the only thing running on this system there was exactly one campaign, so its copy lived in a constant in the config file. Sensible at the time. It was the only correct answer.

Going multi-tenant, that constant didn't get deleted. It got demoted into a fallback, so the drafting code ended up with a line meaning "use this workspace's story, or if that's empty, use the default one." Every tenant's copy now had a silent understudy, and the understudy was one real customer's material.

The trigger was separate and duller. The chat that collects your details had gathered everything correctly, and the submit endpoint never wrote those details down, so the stored request had no sender data at all. The story field resolved to empty, empty triggered the fallback, and the fallback was a race car.

Two correct-looking pieces of code, and the handoff between them dropped the payload on the floor. That is why nothing failed loudly.

The part that actually bothered me

Here is what I found while cleaning up, and it is worse than the bug.

From Rig To Road's own signature field was empty too, and always had been. Every email that workspace ever sent used the fallback signature rather than its own.

Nobody noticed, including me, because the fallback happened to be the same person. It was right by coincidence. The system had been substituting a default for real data for weeks and produced correct output every time, so there was no signal anywhere.

That's the lesson, and it generalises well past cold email:

A default that is correct for your first customer is invisible until your second.

Every solo founder who built for one user and then went multi-tenant has some version of this right now, and you can't see it for the same reason I couldn't. The condition that makes it wrong has never occurred, so your tests pass and your output looks right.

Three things to go check today

If you're shipping something multi-tenant that was single-tenant before, this is twenty minutes.

Look for constants that contain content rather than shape. A default page size is fine. A default company name, value proposition, signature or sender is a landmine. Search for anything named DEFAULT_ or FALLBACK_ and ask whether it holds a real customer's words.

Check every fallback where the left-hand side comes from tenant data. In most languages an empty string takes that fallback as readily as a missing one, and empty is the common case in production and the rare case in your tests.

Test two tenants at once and assert on the actual strings. Not that the output is well-formed. That tenant A's words never appear in tenant B's output. Our test now runs 40 interleaved drafts across two tenants and fails if the racing copy lands in the wrong one, and it greps the compiled build for those strings so the constant cannot come back.

The fix was to delete the default outright. The drafter now refuses to write anything if the sender's story, call to action or signature is missing, and names the field. The only fallback left derives from the sending tenant's own identity.

That has a cost worth being honest about. Failing closed inverts the failure mode: a missing field used to produce a wrong email, and now it produces no email, which is correct, and silent, until somebody looks at the queue. So the same day I shipped it I audited every live workspace for missing fields and put the gap check on the admin dashboard. Fail-closed without a monitor is just a quieter outage.

Nobody was harmed here. The only reason there was no customer to harm is that I don't have customers yet, which is not a control I'd recommend.

If you'd rather see what the thing does than read about it, tell us who you sell to and we'll send you three real decision-makers in your market plus the exact email we'd send the first one, free: apexoutreach.app/find. No signup, no card. It writes your pitch now.

Let ApexOutreach run this for you.
It finds the right prospects, writes personalized emails, sends from your own inbox, and handles replies and follow-ups automatically.
Start a free trial
← All posts