Build in Public
A06

Chapter 6: Post-Launch Polish

6 min read · 1,300 words ·

The first thing that broke was the images.

Not immediately — it took a few posts going out before I actually looked at one and thought: that's wrong. The background was supposed to be Void Black (#090b14) with the pink-to-coral gradient that Zo.E uses as her signature palette. What I was looking at instead was teal-green and amber-gold. The old SSC brand colours. The AI had been generating images against a prompt I'd never updated, and they'd been going out like that, silently, without me noticing, because I'd been too focused on everything else during the launch sprint.

That's post-launch polish in miniature. The machine is running. It's just running slightly wrong.


The category of bug you can't find in testing

There's a specific type of problem you only discover when the system is live and doing real work. It's different from the bugs you catch in QA, and it's worth being clear about why.

Testing is a controlled environment. You're asking: does this do what I designed it to do? Production is something else. Production is: does this hold up when the inputs are real, the timing is irregular, the volumes are actual, and two separate workflows that were never tested together are both running at the same time?

Four of the fixes in this polish phase were that second category. The webhook collision — where the Nunlimited version of the ad-hoc content workflow had somehow been given the same path (starla-adhoc) as the SSC version, meaning both were pointing at the same door — that's not something you'd catch without running both systems in parallel. The WF3 crash that turned out to be caused by WF4 answering a Telegram callback query before forwarding it on: that's a race condition in the handoff between two workflows, and it only surfaces when the system is processing fast enough to create a conflict. The rejection path that silently died — post rejected via Telegram, NocoDB updated, nothing else — that one I noticed because I rejected a test post and then went looking for it in the queue and it wasn't anywhere.

None of these are evidence of sloppy building. They're evidence that production is different from testing. The only way to find them is to run the thing.


The webhook collision was almost funny

Almost. The Nunlimited ad-hoc workflow and the SSC one had both been given the path starla-adhoc. In n8n, webhook paths are global — two workflows sharing a path means whichever one activates first wins the incoming request. In practice, it meant that sending an ad-hoc content request for either brand was a coin flip as to which system would handle it.

The fix was five minutes: rename the Nunlimited path to nnltd-zoe-adhoc. The discovery was more annoying — I'd sent an ad-hoc request that I was reasonably confident was routed to Nunlimited, watched it come out looking like SSC content, spent a bit of time staring at the logic before the penny dropped that the problem was upstream of all of it.

The rejection path fix was more involved. WF4 was handling the reject correctly — writing the status to NocoDB — but I'd never wired what should happen next. There was no regen trigger. The rejection just sat there, terminal and useless. I had to build that connection out properly: WF4 on rejection now forwards to WF3's full regeneration pipeline, which picks up the brief, generates a replacement post, and puts it back in the approval queue. The system can now disagree with me.


Extending to Nunlimited was the bigger piece of work

Zo.E was originally built for SSC. One brand, one NocoDB table structure, one Telegram bot, one set of routing logic. Extending the system to run the Nunlimited brand in parallel required something more substantial than patching a webhook path.

The shared workflows — WF6 and WF9, which handle publishing and analytics — needed to know, at runtime, which brand's data they were working with. The solution was a Code node that detects the brand context from the incoming data and routes accordingly: different NocoDB tables, different Telegram bot credentials, different publishing targets. It's not elegant in the way a properly architected multi-tenant system would be elegant, but it works, and it works without duplicating the entire workflow stack — which was the alternative.

Dual-brand routing is one of those things that sounds simple until you're doing it. Every node that touches a NocoDB table needs to be conditional. Every Telegram message needs to go to the right bot. The brand context has to be threaded through the entire execution chain without getting lost. Getting that right took a full session and a handful of test runs before I was confident it was solid.


The Notion cleanup was a different kind of problem

After launch, Notion was a mess. Not broken — just inconsistent in the way that things get inconsistent when you're building fast. Pages had task-status columns that belonged in the Tasks database. Workflows were listed in multiple places with slightly different names. A couple of pages had information that directly contradicted each other.

None of this was causing any system failures. But it was causing friction every time I opened Notion and had to figure out which page was the authoritative version of something, or where I was supposed to log a task, or whether the workflow listed as "active" was actually the current one.

I spent a session stripping task columns from wiki pages, pointing everything to a single Tasks database, and adding charter callouts to the two workspaces — Nunlimited HQ (audience: entrepreneurs) and SSC Workspace (audience: sales professionals) — to make it explicit which is which. That last bit matters more than it sounds. The two-brand structure is deliberate. The content strategies don't overlap. Keeping them visually and operationally distinct in the workspace means I'm less likely to blur them when I'm working quickly.

The Notion cleanup is a good metaphor for the phase as a whole: none of it was urgent, most of it was necessary, and all of it was the kind of thing you can only really see clearly once the system is running.


How much of this was anticipated

Honestly? Most of the specific bugs weren't. The category was.

I knew going into the launch that there would be a polish phase. Any system of this complexity, with this many moving parts, is going to have things that only surface in production. I'd have been surprised if it hadn't. What I didn't predict was which things specifically — the webhook collision, the silent rejection path, the image prompt lag. You can't predict the specific failures in advance; you can only build something that makes the failures visible and fixable when they arrive.

The Nunlimited extension was more anticipated — I knew from the start that dual-brand support was coming, and I'd built with it in mind even if I hadn't fully implemented it. The Notion restructure was less anticipated, which is a bit embarrassing to admit. I should have set the operational structure up more deliberately at the start. I didn't, and so I cleaned it up afterwards. That's fine. It's done now.

Launch is a checkpoint. The polish phase is not an embarrassing epilogue to the build — it's a normal part of it. The machine is running properly now. The images look right. The reject path works. Both brands are routing to the right places.

Chapter 7 gets into the system itself properly: the architecture of Zo.E, how the ten workflows fit together, and what I'd do differently if I were designing it from scratch. The technical deep dive.

Ta,

James
Founder | Nunlimited

James Nunn signature