AI made the rewrite fast. Encoding our conventions as rules the AI had to follow is what made it consistent.
Every engineering team eventually reaches the point where a growing codebase starts slowing development down. Years of small changes, inconsistent patterns and technical debt make even simple features harder to build, and new engineers spend weeks learning how the application fits together.
Meanwhile, user expectations keep moving. We wanted to modernize the UX of our main React application to improve engagement and the overall experience, pay down technical debt, and build a foundation we could scale on. The obvious answer, a full rewrite, was the one we most wanted to avoid.
TL;DR
- We rebuilt our frontend in the same repository as the legacy app, behind a runtime toggle, with Claude doing much of the implementation.
- Consistency came from encoding our conventions as a skill-based rule system (
CLAUDE.mdplus focused skills), not from the AI itself. - A shared widget layer, a Figma-to-code workflow over MCP, and a review checklist built from the same rules kept the output on-system. The widget layer is now a standalone package used by other products.
The problem with rewriting a large React app
A large frontend rewrite usually runs into four problems:
- The roadmap freeze. Time spent rebuilding is time not spent on customer-facing features, and the business waits for the rewrite to finish.
- The consistency problem. Different engineers make different implementation choices, even when they follow the same general guidelines.
- The big-bang risk. Releasing an entirely new application at once turns every missing feature or behavior difference into a production risk.
- The knowledge bottleneck. The engineers who understand the legacy application best are usually the same ones needed to build the replacement.
The challenge is not simply writing better code. It is building the replacement quickly, consistently and safely enough to actually finish it.
Why AI alone doesn’t solve it
AI is very good at writing React code quickly. That addresses speed, but not the harder problems. Left to its defaults, an AI assistant reaches for the patterns it knows best (raw MUI components, inline styles, loose types), and each engineer’s session drifts in its own direction. Across months of work and multiple engineers, that drift recreates exactly the inconsistency a rewrite is meant to remove.
AI also can’t preserve behavior it can’t see. Much of what a mature application does lives only in its code: edge cases and business rules that were never written down anywhere else. A rewrite in a fresh repository cuts the AI off from that source of truth.
So we needed three things: access to the legacy behavior without inheriting the legacy patterns, a way to make conventions binding rather than optional, and a way to ship incrementally.
How we approached it
Same repository, new UX
We kept the new UX in the same repository as the legacy application. The new code lives under its own top-level directory, and the existing code stayed untouched. A runtime flag decides which application is rendered:
const isNewUXEnabled = true;
isNewUXEnabled ? <NewUXApp /> : <OldApp />;Keeping both versions side by side gave us an important advantage: Claude could read the existing implementation to understand business behavior and edge cases that were never documented elsewhere.
The new UX had one strict rule: new code could reference the old code for understanding, but could never import from it. That gave us a clean boundary between the two applications while still letting the AI treat the legacy implementation as a source of truth.
The toggle also let users move between the old and new experiences during the migration, so features shipped incrementally instead of waiting for the whole rewrite. And because the two trees were isolated at the module level, Vite kept their dependency graphs separate: the legacy code was never included in the new UX bundle.
Conventions as a skill-based rule system
We put our engineering conventions directly into the repository. A CLAUDE.md file became Claude’s persistent context, describing the technology stack, folder structure, architectural rules, isolation requirements and shared widget system. Engineers no longer had to re-explain these at the start of every session.
Instead of one large document, we split the rules into focused skills:
- routing
- state management
- data fetching
- styling
- TypeScript
- component architecture
- performance
- accessibility
Each session loads only the guidance relevant to the task, without unnecessary context. The rules are explicit, for example:
- no
any - no hardcoded colors
- no unnecessary constants files
The goal was to turn “the AI should know this” into “the AI has to follow this.” It made a noticeable difference: developers with very different coding styles produced code that followed the same conventions, because those conventions lived in the shared rule system rather than in individual habits.
Building in phases
We built in phases. First came the shared foundation: the widget layer, layout shell, routing and the other infrastructure every feature depends on. Next we centralized authentication, so new features used one consistent auth layer instead of carrying forward scattered legacy patterns. Then we rebuilt the product feature by feature on top of that foundation.
We also used the rewrite to eliminate a long-standing TypeScript problem: $TSFixMe, effectively an alias for any that had accumulated throughout the legacy codebase. We removed it entirely from the new code and replaced each case with a specific type. It wasn’t a visible feature, but it removed a whole category of hidden type-safety problems.
Making shared components AI-friendly
One of our first realizations was that the AI needed to understand how we build UI in our codebase, not just how to write React. We standardized common UI patterns (inputs, selects, tables, buttons, modals, tooltips, empty states) into reusable components such as InputWidget, SelectWidget and TableWidget.
The harder part was getting Claude to use them consistently instead of creating something new. So we added those decisions to the rules Claude loads. For every widget we documented what it is for, when to use it, and, importantly, what not to use instead. For example:
- use
InputWidgetinstead of a raw MUITextField - use
DeleteButtoninstead of building a custom trash icon
This became one of our most important guardrails. Without explicit rules, AI falls back to familiar MUI or HTML components. Documenting both the preferred component and the alternatives to avoid gave it enough context to follow our design system.
Connecting Claude to Figma through MCP
What made the approach practical day to day was connecting Claude directly to Figma through MCP (the Model Context Protocol). Instead of working from screenshots or manual inspection, Claude reads the actual Figma structure, including spacing, typography and design tokens.
It then implements the design while following our widget registry, theme rules, spacing conventions and TypeScript standards. The result is not only visually close to the design, but also consistent with the rest of the codebase.
Reviewing against the same rules
The rules used to generate code were also used to review it. Our review checklist covers the same things Claude follows during implementation:
- correct widget usage
- no hardcoded values
- no
$TSFixMe - accurate implementation of the Figma design
That creates a consistent loop:
Define the rule → Generate the code → Review against the same rule
Where we are now
The new UX is now the primary experience, and the old UX has been retired. It is no longer reachable at runtime and was never part of the new UX bundle.
Beyond the modernized frontend, the outcomes we care most about are structural:
- Developers with different coding styles follow the same conventions, use the same widgets and make consistent architectural decisions, because those rules are built into the AI workflow.
- Authentication is centralized, and long-standing TypeScript debt such as
$TSFixMeis gone from the new code. - Figma designs turn into production-ready code with much less rework.
The widget layer also outgrew the rewrite. Once it was stable, we moved it into its own npm package as a standalone RapidCanvas UI widget library. Other products now use it too, including customer-facing DataApps, so the same buttons, inputs, tables and modals share one design language instead of every team building its own.
Lessons for teams considering the same approach
If another team wanted to try this, these are the questions we would ask first.
Can you keep the old and new applications in the same repository? Use a strict one-way reference rule. It gives the AI access to existing behavior without letting legacy patterns into the new implementation.
Is there a place where engineering conventions are automatically available to the AI? A CLAUDE.md equivalent with a structured rule or skill system is far more useful than documentation that engineers and the AI have to remember to find.
Is your component library documented well enough for the AI to use it correctly? The AI needs to know that an internal widget exists, why it should be used, and what it should not be replaced with.
The trade-off is up-front investment: the rule system, the widget documentation and the shared foundation all had to exist before feature work could move quickly. That investment is what kept months of AI-assisted work consistent.
What’s next
The biggest lesson was that AI did not make the rewrite succeed by writing code faster. It worked because we gave the AI what we would give any good engineer joining the team: clear documentation, explicit conventions, and a process that consistently enforces them. That makes the approach useful well beyond a frontend rewrite, and we’ll keep applying it as the shared widget library grows across our products.
If you’re weighing a similar modernization for your own applications, we’d be glad to compare notes: book a discovery call.





