The First Attempt Failed
In September 2023, we tried to modernize the dashboard. It didn't work.
The codebase was CodeIgniter with jQuery. Stylesheets were a mix of SASS and LESS that nobody fully understood. The UI had grown organically over years, screen by screen, with no shared language for how things should look or behave. We knew it needed to change.
So we started building. And we stalled almost immediately.
The problem wasn't technical. We had the skills to write React components. The problem was that nobody was making UX decisions. Every screen raised questions that nobody had authority to answer. What should the navigation look like? How should forms behave? What's the interaction pattern for a user managing multiple trading accounts? Without dedicated UX leadership, every component became a debate. Progress ground to nothing.
The project quietly died. We went back to maintaining the jQuery codebase.
The Restart
Fourteen months later, in November 2024, we tried again. This time the team restructured. There was proper UX leadership. Clear design direction. Decisions got made.
I was the sole frontend developer. The backend team was four developers, all strong in their domain but trained on jQuery patterns. They'd need to contribute to the new frontend eventually. That constraint shaped everything I built.
The Constraint Landscape
Here's what made this interesting, and by interesting I mean stressful.
TitanFX is a live trading platform. Thousands of people use it every day to manage real money. Forex traders in Asia logging in at 2am. Account managers running reports. People depositing and withdrawing funds. The dashboard is not a content site you can break for a weekend and fix on Monday. If a page fails, someone can't access their money.
Zero tolerance for breakage. Zero.
And this wasn't a greenfield rewrite where you build the new thing and flip a switch. We couldn't stop the world. The old system had to keep running while the new system grew beside it. Page by page. Feature by feature.
I've heard people describe this as "changing the engine while the plane is flying." That's dramatic but not wrong. I'd call it a live heart transplant. You can't stop the heart. You can't rush. You just have to be methodical and not panic when something bleeds.
Storybook Was a Survival Mechanism
I didn't adopt Storybook because it's a best practice. I adopted it because I was one person and the backend team needed to contribute to frontend code without me being a bottleneck.
The backend developers knew jQuery. They were used to grabbing a DOM element and mutating it. React's mental model -- props down, events up, state management, component composition -- was foreign. I couldn't spend weeks teaching React fundamentals. We had a migration to ship.
Storybook became the translation layer. Every component I built, I documented in Storybook with its props, its variants, its expected behavior. When a backend developer needed to build a page using existing components, they opened Storybook and saw exactly what each piece expected. No guessing. No asking me. No meetings about "how does this button work."
This was political as much as technical. In a team where one person owns the component layer and four people need to consume it, you either create a self-serve system or you become the bottleneck that kills the project. Storybook made me replaceable in the best possible way. Anyone could look at the component library, understand what was available, and compose pages from it.
It also caught something I didn't expect. When the backend team reviewed components in Storybook, they'd spot edge cases I missed. "What happens when the account name is 40 characters?" "This dropdown doesn't handle the case where a user has zero trading accounts." The visual, interactive catalog invited feedback that code reviews never would have.
Running Two Systems
The unglamorous reality of incremental migration is that you maintain two systems. For months.
The old CodeIgniter app served most pages. The new Next.js app served the pages we'd migrated. A routing layer decided which system handled each request. Users didn't know. They'd click a link and maybe they were on the old system, maybe the new one. The experience had to be indistinguishable.
Deciding what to migrate next was never purely technical. The obvious answer is "start with the simplest page." But the simplest page is also the one nobody cares about. We needed to show value early. So we picked pages that were high-traffic and high-pain -- pages where the old system was slowest or where users complained most. That meant taking on more risk upfront. A quiet settings page would have been safer. We went after the account overview instead.
Every page launch followed the same pattern. Build it. Test it. Route 5% of traffic to it. Watch the error logs. Watch support tickets. Wait. Increase to 25%. Wait again. Full rollout. Hold your breath for a week.
Some launches were clean. Some weren't. One page had a date formatting issue that only showed up for users in a timezone we hadn't tested. Another had a subtle CSS difference that made a button look disabled when it wasn't. Small things. But on a trading platform, a button that looks disabled when it controls a withdrawal is not a small thing.
We fixed them fast. That was the advantage of the new stack. In jQuery, tracking down a rendering bug meant grepping through thousands of lines of procedural code. In React with TypeScript, the component tree told you exactly where to look.
What I Carried Forward
Eight months of migrating a live platform taught me things that don't fit in a bullet-point list, but I'll try anyway.
Migration is a team sport disguised as a technical exercise. The hard part was never "convert jQuery to React." The hard part was getting four backend developers comfortable enough with a new paradigm to contribute without fear. Technical decisions that ignored this reality would have failed no matter how architecturally sound they were.
UX leadership is not optional. The first attempt proved this. You can have talented developers and a clear technical vision and still go nowhere if nobody can make design decisions with authority. The fourteen months between our failed first attempt and the successful restart weren't wasted -- they were the time it took for the organization to understand this.
Incremental beats big-bang, but incremental is harder. Everyone talks about incremental migration like it's the safe, easy option. It's not easy. It means maintaining two systems. It means routing complexity. It means edge cases where the old system and new system interact in ways you didn't predict. It's the right approach for a live platform, but don't let anyone tell you it's the comfortable one.
Patience is a technical skill. I wanted to migrate everything at once. I wanted to delete the jQuery code and never look at it again. But the platform didn't care about my preferences. It cared about uptime. Moving slowly and deliberately, page by page, test by test, was the discipline that made this work.
The Numbers
Oh, and page loads went from 3.2s to 1.3s. Bundle optimization, code splitting, server-side rendering -- the usual Next.js wins. TypeScript eliminated an entire class of runtime errors that the jQuery codebase produced weekly. New components that used to take hours to build from scratch now took minutes with the shadcn/ui foundation.
But that wasn't the hard part. The hard part was everything above.
Related Reading
- Three Tools I Built Because Someone on the Team Was Stuck — The internal tools I built at TitanFX alongside this migration