Background
An appointment reminder platform for orthodontic and dental practices, with patient messaging and online scheduling built around it. The backend is Rails and the frontend is React. The product is proprietary, so this case study does not describe its features or show its interface.
The problem
The frontend dated to 2016. By 2025 it was React 15 with class components, a pre-1.0 component library, and a build chain several major versions behind on every tool. The backend had been through its own runtime and framework upgrades over the years, but the frontend had only ever been added to. Customers we interviewed described the interface as disjointed, and each new feature cost more than the one before it.
Leadership wanted new capabilities, not just a cleaner version of the existing ones. Several of those capabilities changed fundamental parts of the application: how users were modeled, how permissions worked, and how the API authenticated and reported errors. That required a new API version, which in turn meant the backend had to be upgraded before the new frontend could exist.
Constraints
I was the only frontend developer on the project.
The existing product had to keep serving customers, unchanged, for the duration.
The new frontend depended on an API that did not yet exist and was being built in parallel.
The old frontend had very little test coverage, so regressions were usually discovered by customers.
The target was six months to parity with the old product and a year for the new capabilities.
Approach
We decided against replacing the old frontend one page at a time. The new capabilities changed the API, login, permissions, and components shared across every page, so there was nothing stable for old and new pages to share, and any bridge between them would have been months of extra work.
We also ruled out upgrading in place. It would have left us with a modern build of the same disjointed interface, and the first few steps alone would have cost nearly as much as a rewrite.
So ultimately we chose to rewrite, as a separate application against the new API, leaving the old product untouched. The cost of that choice was clear from the start: no real users would see the new frontend until it was ready, so feedback would arrive late. Much of the process below exists to compensate for that.
How we built it
The project began with a forced ranked meeting that included leadership, sales, support, and our full-stack engineering team. I then ran structured interviews with daily users at several practices, writing each one up in the same format so they could be merged. Every requirement in the synthesized research carries an evidence line quoting the interview it came from, which kept later scope discussions short.
From the research I wrote a single brief describing the product for a design tool: primary screens, layout regions, non-goals, and principles. Wireframes were produced from the brief and the Figma design from the wireframes. Design tokens were defined in Figma and mapped one-to-one to the code’s theme, so no color values exist downstream of the design file.
The frontend was built ahead of the backend. Every API endpoint was mocked with a service worker that behaved like a real backend, including pagination, filtering, and error cases, and the frontend was built and tested against those mocks while the real API was written. When endpoints began landing piecemeal, I added a live-first mode: the real API is used by default, with the mock as a fallback only for routes the backend does not yet have, and no allowlist to maintain. The cutover happened one route at a time with no coordinated release. A benefit I did not fully anticipate is that the backend team received a frontend built against the contract they intended, rather than against whatever the old interface happened to need. We now build exactly what the product requires.
Testing was in place from the first commit. Unit and integration tests, end-to-end tests, and the API mocks all run in CI on every pull request, and passing them is a condition of merging.
The agent workflow
We used an AI-assisted workflow for most of the build, with the setup living in the repository. An instructions file describes the architecture, conventions, and scripts. Rules that come up repeatedly, such as how components are split into primitives and composites and how tests are structured, are written as skills the agent loads when they apply. It is blocked from reading generated files so its context isn’t spent on them, and a kanban file in the repository carries tasks between sessions.
Each non-trivial feature starts with a written proposal that pins the Figma node for the screen. The agent reads the design from Figma directly, maps Figma tokens to the project’s existing tokens, and flags anything that doesn’t map. The data layer is built first, with schemas, hooks, and mocks, and the UI follows as a separate change. Tests run in CI on every pull request, and a person reviews each one before merge.
What went wrong
The agent did not always follow the conventions, and I was finding the drift in review: local fixes, inconsistencies, and patterns that didn’t match the rest of the codebase. Catching them one pull request at a time was not keeping up, so I built an audit workflow that runs several review passes in parallel, adversarially verifies each finding, and writes the results as backlog cards with file and line evidence. The first run found 73 issues. They were remediated in a series of pull requests, and the rule since then is that every new finding also indicates a convention or skill that needs sharpening. The audit re-runs after roughly every ten feature pull requests.
Results
The new frontend reached its first demo walkthrough on schedule and is in staging ahead of beta scheduled for Q1 2027. The old product has run unchanged throughout.
Looking back
I would put the Figma design in front of two users before writing the first spec, rather than after the first demo. Late feedback was a cost I chose, and it was still higher than I expected. I would also start the drift audit in the first week instead of the sixth.