Why Every FleekDash Update Broke Something "Unrelated" — And How We Rebuilt the Architecture to Stop It
Why Every FleekDash Update Broke Something "Unrelated" — And How We Rebuilt the Architecture to Stop It
Published: September 23, 2026 Tags: architecture, refactoring, enterprise, AI agents, regression testing, FleekDash
The pattern nobody wants to admit
Every release cycle looked the same.
We would ship a feature — say, a theme improvement. Tests green, review clean, demo perfect. Then, two days later, a ticket lands:
"The media library drag-and-drop stopped working after the update."
The media library. We didn't touch the media library. And yet, somehow, we did.
Each incident was small on its own. A one-line fix here, a guard clause there. But the pattern was exhausting: every release generated tickets in parts of the app we never edited. Our team started dreading launch day. Users started dreading the update button.
That is the moment you stop asking "how do we fix this bug?" and start asking "why does our codebase let this class of bug exist at all?"
So we audited everything. The answer was uncomfortable.
The audit: 473 invisible wires
FleekDash is a hybrid app — a React SPA embedded in wp-admin, talking to a PHP REST layer, wrapped in an iframe window system. Years of fast iteration had produced something that worked but had no enforced structure.
We wrote a gate to measure it. The number: 473 boundary violations. Modules importing sideways across layers, domains reaching into each other's internals, shared utilities pulled in from five directions at once.
Here is the thing about that number: every single one of those violations compiled. TypeScript was happy. Tests were green. The code worked — until you moved a file, and a screen you never opened exploded in production.
The architecture looked layered. It wasn't. It was a plate of spaghetti with good naming conventions.
What we thought we had: What we actually had:
Platform ──────────► Platform ──┐
↓ ↓ │
Design System DS ←─────┤ (imports going UP)
↓ ↓ │
Window System Domains ←┼── (reaching sideways
↓ ↓ │ into siblings)
Domains ─────────┘
When "unrelated" screens broke, they weren't unrelated. They shared invisible wiring — a util that five features imported, a type that crossed domain borders, a chunk that bundled eager and lazy code together.
The rebuild: layers you can actually enforce
We picked a direction and made it a law, not a guideline:
Platform → Design System → Platform UI → Window System
→ WordPress Bridge → Domains → App
The key word is enforced. We added an ESLint boundary gate (import/no-restricted-paths driven by a zones config) plus a ratchet baseline: existing violations are recorded, new ones fail the build. Then we burned the list down across phased refactors — each phase a separate, reviewable commit range.
The result so far:
Read that last row again. During the cleanup we discovered that a color-picker subtree had silently landed in the chunk that loads eagerly at login. Moving it to the right layer cut 794 kB of JavaScript out of the login path. Nobody set out to optimize performance — correct structure produced performance.
That is the thesis of the whole project: the violations weren't the disease; they were the X-ray of the disease.
The machinery that keeps it from regressing
Fixing the code was the easy half. The hard half is making sure next month's release doesn't re-break a screen nobody opened. This is what we built around it:
1. The launch gate that clicks the screens for you
Before a version ships, a script boots the app against a real local WordPress, logs in, and walks the ten golden paths — dashboard, plugins, posts, settings, users, integrations, the WooCommerce screens, even the wp-admin iframe bridge. It fails the launch on any console error or a blank render. It is the automated equivalent of a human clicking through the product before shipping. We literally watched it catch a broken iframe navigation that every static gate had missed.
2. A client error beacon
Users shouldn't be our monitoring. A tiny listener captures runtime errors and unhandled rejections, dedupes and batches them (max a few requests per session — every request costs a full WordPress boot on shared hosting), and posts them to a REST endpoint. After a release, we see real user errors aggregated by route and version in minutes — not in ticket form three days later.
3. Blast-radius reports
One script, audit:blast-radius, builds the import graph and answers the question every reviewer actually needs: "this diff touched 12 files — what can it break?" The answer, transitively: which layers, which domains, which actual pages are in the fan-in. A one-line change to a shared formatter stops looking like a one-line change.
4. A refactor protocol that mimics a careful human
Before moving a module: inventory every importer (including the ones TypeScript can't see — service-worker precache keys, PHP strings, shell scripts). Write down what the code does beyond its types — side effects, registrations, init order. After: every importer resolves, rg <old-path> returns zero, the domain's tests run. Simple. Written down. Mandatory.
5. A ratchet on per-request cost
Every apiClient.* call site is ratcheted against a baseline file. A new call site fails CI. On shared hosting, each API call is a full PHP boot — this single gate has probably prevented more 508s than any optimization we ever shipped.
The part we didn't expect: the agents policed each other
Here is where the story gets weird. This refactor was executed largely by AI coding agents — local sessions plus cloud review agents from Cursor and Devin, with Qodo and Kilo bots reviewing PRs incrementally.
What we learned: the gates catch code errors; only independent review catches judgment errors. A move can be technically perfect — every import resolves, every test passes — and still be wrong in a way only a fresh-eyes reviewer sees. So after every large refactor commit, we dispatch a regression-hunt: a cloud agent that gets the diff range, checks out HEAD, and verifies existence against the real tree rather than trusting the diff.
It works almost embarrassingly well:
CSS.Translate back to CSS.Transform in three grid managers — reintroducing a drag-deformation bug we had already fixed once months earlier. It also caught a missing activationConstraint that made clicking a rename button start a drag instead.Each of those would have shipped. None of them would have been caught by types, tests, or lint.
And yes, the agents collide — we made rules for that too
When multiple agents share one repo, they share state you don't think about. Yesterday one agent's git stash — which is global across all worktrees, not per-worktree — got popped by a different agent onto the wrong branch, and nearly clobbered live work mid-flight. The recovery was clean, but the lesson is now a written rule: no git stash in multi-agent flows; use a throwaway worktree or git show HEAD:file instead.
Same story with branch checkouts in the shared main worktree (banned — agents get worktrees), save-conflicts from stale editor buffers (check git log before overwriting — the "conflict" was usually a teammate-agent's newer commit), and review bots pushing commits after the last green gate run (now gated: you must acknowledge bot commits against the exact head SHA before merge).
The meta-lesson underneath all of it: every incident becomes a rule, and prose rules keep failing until they're mechanical. A paragraph in a doc gets ignored. A script that exits 1 gets obeyed — by humans and agents.
So… is the app better?
Honest answer, because we promised ourselves honesty when we started:
Visually? No. Same product, same screens, zero new features. If you're waiting for a reveal, there isn't one.
Measurably? Yes. The login path ships a third of the JavaScript it used to. The production build passes a chunk-integrity gate it was failing a week ago. Three separate regression classes that would have become tickets got caught and fixed before merge.
But the number that actually matters doesn't exist yet. It's the regression-ticket count on the next release. That was the pain that started this whole thing — updates that broke "unrelated" screens, the tension on the team, the hesitation users felt before clicking update.
We built the architecture that makes those regressions structurally harder to create, and the machinery that catches them when they happen anyway. If the next release is boring — no surprise tickets, no emergency patch — that's the payoff.
Boring releases are the feature. That's the whole point.
FleekDash is a self-hosted WordPress admin shell — React SPA + PHP, designed for agencies who live in wp-admin all day. If you've ever been afraid of your own update button, you understand why this mattered.
