← All projects

Rebuilding TypingMentor: Taking Ownership of an Eight-Year-Old Product and Rethinking Its Foundation

Taking over TypingMentor

TypingMentor has been around since September 3, 2018. It was already a working product long before I touched it. I joined Intellixio on January 15, 2025, and took full engineering ownership of TypingMentor. That meant I was not starting with a blank idea. I was responsible for an existing product with history, traffic, users, and behavior that already worked.

The question was not, “Can I build a typing website?” It was, “What should this product become next, and is the existing technical foundation good enough to take it there?” By 2026, I decided incremental changes were no longer enough. I started a full rebuild. I wanted to preserve what mattered most, the typing experience, while reconsidering the architecture around it: rendering, lessons, accounts, profiles, leaderboards, SEO, subscriptions, testing, performance measurement, and eventually AI-powered coaching. A lot of the decisions did not survive their first implementation. That became one of the most valuable parts of the project.

Engineering the rebuild

Typing applications have an unusual constraint: the most important interaction happens continuously. Every keypress changes state, updates characters, advances the caret, affects statistics, and may trigger other UI changes. Even small rendering problems become noticeable very quickly.

I decided not to make React the authority for the typing hot path. The mutable typing engine owns input, validation, progression, timing, and test state. Zustand and React receive snapshots for the parts of the product that need reactive UI. The trade-off is that I have two representations of state to keep synchronized. That is more specialized than a normal React application, but it lets me control exactly what happens during a keystroke.

I also virtualized the typing surface. Instead of rendering an entire long test, TypingMentor mounts only a small window of visible lines. Later benchmarks gave me a way to check whether those decisions continued to hold as the product grew. Stored synthetic production runs record about 59.88 FPS and 0.8 to 1.5 ms visual key-to-paint.

Early in the rebuild, I used Ant Design. It gave me common UI components quickly, but it also brought providers, runtime theming, CSS-in-JS, and generic component behavior into an application where I wanted tight control over the main interaction. I eventually removed it and built the smaller set of primitives TypingMentor actually needed. That meant taking responsibility for things such as keyboard behavior, focus management, portals, overlays, and scroll locking. The measurable result was a 184.7 KB gzip reduction in client chunks, about 6.8 percent.

I made a similar reversal with content. I introduced Sanity for lessons and removed it four days later. I replaced it with typed content inside the repository, static page generation, and external JSON for larger corpora. The lesson for me was not that component libraries or CMSs are bad. The lesson was that I should not introduce another system just because it could become useful later. I now try to distinguish much more carefully between a real requirement and a possible future requirement.

The leaderboard changed the way I think about scaling. My first approach precomputed global rankings. I stored rank separately, synchronized it with database triggers, and refreshed it on a schedule. It worked, but I had created duplicated state, background jobs, synchronization rules, and rankings that could become stale. Instead of optimizing that architecture, I reconsidered the product requirement. I removed the stored ranking system and moved back to one source of truth with ranking calculated on read. The leaderboard later became based on comparable official Test sessions within a rolling 24-hour period, and the visible product only needed a top 25.

That was a useful shift for me. I could have built increasingly sophisticated infrastructure around global rankings. Instead, I reduced the problem until most of that infrastructure was unnecessary. Sometimes the best scaling optimization is deleting the scaling problem.

Building Mentor+

When I started building Mentor+, I did not want to put a chatbot next to the typing test and call that AI integration. I wanted the AI to work through the product. If Mentor+ says someone is struggling with a specific area, that should come from real TypingMentor data. If it recommends practice, the user should be able to move directly from that conversation into the existing typing experience.

So I gave the model narrow tools. It can retrieve coaching information, recommend catalog lessons, generate personalized practice, attach practice to a conversation, and suggest an official Test. The model is not supposed to invent statistics or pretend that a lesson exists when the corresponding tool did not succeed. I originally built personalized lesson generation as a separate API endpoint. After seeing the interaction, I removed it and turned lesson generation into a conversational tool instead. That also meant I did not need an AI-specific practice runner. Mentor+ lessons use the same typing engine and results system as the rest of TypingMentor. The LLM itself became replaceable. The integration with the product is the part I consider important.

Personalized lessons can take time to generate, so I tried preparing the next lesson before the user clicked Continue. This improved the interaction when the lesson was ready, but the first implementation had incomplete lifecycle semantics. What should happen if the user chooses Repeat? What happens if they leave before generation finishes? What if a completed lesson belongs to an older request? The first version did not handle all of those cases safely. I revised it so Continue joins the correct in-flight generation, Repeat invalidates prepared work, stale cards cannot attach to newer conversation state, and analytics events do not get counted twice.

That gave me a rule I now apply to speculative work generally: Starting work early also means designing identity, ownership, cancellation, and invalidation early.

Performance in production

I eventually stopped treating Lighthouse as the complete definition of performance. TypingMentor now has a benchmark specifically for the typing hot path. It measures FPS, visual key-to-paint, INP, long tasks, completion, memory, and React rendering behavior. The benchmark itself had to be engineered. At one point a frozen renderer left CI doing nothing useful for 14 minutes. After adding diagnostics, I discovered that the debugging instrumentation itself affected the result. INP changed from 24 ms to 40 ms while that instrumentation was enabled.

I also spent time investigating what appeared to be a slow LCP because Lighthouse showed roughly 3.6 seconds. Direct instrumentation measured observed LCP at only 142 ms. My first explanation was wrong. I kept independently useful changes, such as better asset caching, but I did not pretend the original diagnosis had been correct.

Another optimization looked good numerically and still made the product worse. I disabled SSR for the main playground and saved roughly 41 KB on mobile. On desktop, however, the first viewport was blank while the playground loaded. I tried a skeleton, rejected it, and restored eager server rendering. That reinforced something I now care about much more: performance metrics are evidence, not the product. The loading sequence the user actually experiences still matters more.

By September 2026, the rebuilt TypingMentor was serving meaningful production traffic. Organic search was its largest acquisition channel, with search visibility reaching hundreds of thousands of impressions. That mattered to me because the architecture was no longer being exercised only by benchmarks and test suites. Real production traffic put the search landing pages, lesson content, account flows, and typing experience under the same operating conditions. I could evaluate rendering, SEO, content architecture, authentication, and performance as connected parts of the live product.

By late September, TypingMentor was handling real production traffic and processing subscriptions and advertising monetization. That changed the kind of problems I had to solve. Billing state, entitlements, ad suppression, and access control were no longer isolated features. I had to treat them as part of the reliability of the product itself.

Subscriptions

For subscriptions, I chose Paddle as the billing source of truth. Instead of recreating its complete subscription state machine locally, TypingMentor stored a small mapping between the user and the Paddle subscription. That simplicity introduced a flaw. A second checkout could replace the stored subscription ID while an older subscription was still active in Paddle. The application could effectively lose visibility of a still-billable relationship. I fixed that by checking the existing Paddle subscription before creating another checkout and blocking overlapping live subscriptions. The lesson was simple: delegating state to another system does not delegate my responsibility for the invariants between that system and mine.

What I learned

The project now has 1,951 passing unit and integration tests, while Playwright discovers 115 browser tests across 25 spec files. Desktop Lighthouse performance in the stored runs ranges from 93 to 100. Mobile ranges from 53 to 87, so I do not consider that problem solved.

But the biggest outcome for me was not a particular score or architecture. It was becoming more comfortable changing my mind. I removed technologies I had just introduced. I deleted systems instead of scaling them. I reverted optimizations that improved one metric but hurt the actual experience. I rebuilt AI workflows after discovering lifecycle problems. I learned to separate what I could measure from what I merely believed.

Taking ownership of a product that had existed since 2018 changed the questions I ask as an engineer: Does this complexity actually need to exist? What invariant am I introducing? Am I measuring what the user experiences, or only what is easy to measure? Can I reduce the requirement before scaling the solution? And most importantly, if the evidence proves my original decision wrong, am I willing to delete my own work?

TypingMentor taught me that good engineering is not about being right on the first implementation. It is about building enough understanding, measurement, and flexibility to know when the implementation should change.

Last updated: September 27, 2026