How I Rewrote Econumo from PHP to Go in Three Weeks and Almost Stopped Reading Code

In December 2024 I wrote that I had reevaluated the Econumo stack after three years of maintaining it. I said: “Of course, I won’t take on rewriting the project now, but I’ve drawn conclusions for the future.” A year and a half later, I rewrote the whole thing: the backend from PHP + Symfony to Go, the frontend from Vue to React. Claude Code did almost all of the work, while I steered it and tested.
Here is how it went, and why I now barely read the code that ships to production. I’m not 100% sure this approach works for every product, but the direction is clear to me.
An App I Never Planned to Rewrite
Econumo is a family budgeting app that has been around since 2021. I built the MVP in two weeks on PHP, Symfony, PostgreSQL and Vue, because that was the fastest way, and because I wanted to learn Vue along the way. It had run stably ever since. There was no real reason to change anything.
While working on other products, I noticed I had almost stopped writing code by hand. Agents write it, and I set the tasks and check the results. Yet I was still choosing technologies as if I were writing the code myself: whatever felt familiar. An agent has different criteria.
What an Agent Needs
When a human writes code, they care about convenience, readability and good tooling. An agent needs feedback: fast, unambiguous and ideally automatic. It writes code, immediately gets a compiler error, fixes it, runs the tests, fixes again. The fewer layers of magic between its edit and the error message, the better.
From that angle, PHP stopped working for me. Here are the most concrete reasons:
- Interpreted language. Types, PHPStan and the IDE help, but some problems still surface only when the code actually runs. An agent has a better chance of missing a bug.
- Dependencies. The app rested on Symfony and dozens of packages: 40 direct dependencies and 107 packages in
composer.lock. I hadn’t updated many of them for about two years, because by then an update had become a time-consuming project of its own. You may remember from my previous article how Dependabot once brought an update that broke authorization. - Supply chain. Package ecosystems contain more and more agent-written code. You can’t check every dependency by hand. The fewer you have, the smaller the surface you have to watch.
Why Go
What I want from a language: compilation and a fast compiler, a large standard library, built-in testing, simple syntax, a single binary, and low resource usage. Go fits.
- Simplicity. The syntax is small and uniform.
- The compiler. Invalid code is caught immediately, not at runtime.
- Standard library. Router, HTTP, tests, JSON and cryptography come out of the box. After the migration, the backend has 9 direct dependencies versus 40 in the PHP version.
- A single binary. Easy to deploy, roll back and run in a tiny container.
I considered Rust too. It’s safer and faster, but noticeably more complex, and for this app simplicity matters more than maximum control.
Another discovery came along the way. Organizing a Go app into packages works very well with an agent. When each feature lives in its own package with its own handlers, models, database access and tests, changes stay local. The list of touched packages shows what the agent actually changed, and if a small fix touches ten unrelated packages, that’s a reason to be suspicious. Package structure became a way of reviewing as well.
SQL gets checked before it runs, too. For the database I use sqlc: I write (or rather, the agent writes) plain SQL queries in files, and sqlc turns them into typed Go code. The schema comes from the same migrations that are applied at startup, so a query can’t drift away from the schema. If the agent writes a query against a column that doesn’t exist, or renames a column and forgets about a query, generation fails, and you see it right away, before the tests. For the two databases (SQLite and PostgreSQL), a separate package is generated from the same queries, with separate variants where the SQL really differs. The project now has about 580 named queries, and each of them is verified by the compiler and sqlc, not just by tests.
This is one more step of feedback for the agent: a SQL error is caught as fast and as unambiguously as a type error.
The Trigger
When the Claude Fable model came out, I was impressed by the results of my experiments, and decided to try migrating the project from PHP to Go just for the fun of it. When I saw that it worked, I decided to see it through.
I worked about 20-30 hours a week for three weeks in a row, so roughly 60-90 hours. The first backend PR was opened on June 20, and the beta of the new version shipped on July 9. The only money spent went to subscriptions: Claude Max 20x at $200 a month (I took the biggest plan to have headroom for Fable) and Codex at roughly $25 a month. I had the subscriptions for two months, but finished in about one. I think I used up the entire July limit. There were no separate API costs.
Access to Fable was unstable at the time: it was announced as available for a week or two, then extended. I worked the whole time worrying that access would be taken away before I finished the migration. That is partly why it went so fast.
Features First, Beauty Later
The main rule I set myself: first achieve the same behavior, and only then improve the architecture. If you change logic and structure at the same time, you can’t tell what broke.
- Mechanical translation. The agent ported PHP to Go almost one to one: the same endpoints, request and response formats, business logic, database behavior. The result wasn’t pretty, but it was predictable.
- Tests. I carried them over and extended them. The PHP version had about 7 thousand lines of tests. After the migration there were about 20 thousand. I raised coverage to an 80% threshold and added a check requiring every new route to appear in the test catalog.
- Frozen API contract. The old frontend was not supposed to notice that the backend had changed. The JWT format, password hashing and email encryption were verified against reference responses from the live PHP app and on a copy of the production database.
- Swap on the live app. I connected the Go backend to the same database and the same frontend. Users need no data migration: at startup the binary recognizes the old Doctrine migrations and applies only the new ones.
- Refactoring. Only once the behavior matched did the agent lay the project out into packages, remove unneeded abstractions and replace some third-party packages with its own implementation.
Econumo supports SQLite and PostgreSQL, so the same tests run against both databases. Otherwise an agent can easily introduce a change that works on one and breaks on the other.
The architecture, in short. It used to be a layered app with DDD elements: controllers, services, repositories, domain, infrastructure. It became a feature-based app, where one feature lives in one package.
Frontend: Rewrite, Don’t Port
With the frontend I did it differently. I wrote the old Vue code while learning Vue myself, and a pile of technical debt had built up. There wasn’t much point in porting it as is.
Claude rewrote most of it from scratch in React. It first looked at the existing screens and behavior, recreated the structure, connected it to the unchanged API, and ran UI and screenshot tests. The first pass took about 8 hours, and the agent worked about 15 hours in total. After that I spent two or three days going through all the screens by hand and fixing things. I used Chrome MCP for browser access so that Claude could see how things used to look.
Why React? It has the most examples, components and discussions that models were trained on, and an agent makes fewer mistakes in it. That’s an assumption too, but practice confirms it so far. The code size barely changed: about 21 thousand lines of Vue and TypeScript became 19 thousand lines of React.
By the way, after the migration the frontend has more dependencies, not fewer: from 20 to 38, because there are so many useful libraries like shadcn.
The Numbers
Both versions were measured the same way: Docker with an empty SQLite database, 16 threads hitting the API for 15 seconds. The PHP image is from the last release (v0.10.1), the Go image from the current version (v1.7.0).
- Image. 1.59 GB vs 66.5 MB.
- Memory. PHP: 57 MB after startup and up to 100 MB under load. Go: 14 and 20 MB.
- Speed. PHP: about 193 requests per second at a 79 ms median latency. Go: about 1190 requests per second and 12 ms.
A fun fact: the first Go version (v1.0.0) used 6 MiB at idle and responded faster than the current one (v1.7.0). Over three months MCP, a mobile app, recurring transactions and billing were added, and none of that is free.
How I Review Now
I stopped reading frontend code, and I read the backend selectively:
- Which packages and files changed. The scale of a change is visible immediately. A small feature with a wide footprint is a warning sign.
- What happened to the database. Migrations, dropped columns, renames, new tables, strange indexes. A database mistake is harder to fix.
API contracts, tests, screenshots, CI, Dependabot and model reviews replace everything else.
For the frontend, screenshots turned out to be key, because they turn “looks weird” into a reproducible check.
Observer Mode: Claude Writes, Codex Reviews
My main agent is Claude Code. But having the same model write and check its own code is not great. So I added a skill to Claude with an instruction: when a feature is ready and verified, it doesn’t report “done” but kicks off a review by Codex.
How it works:
- Claude finishes the feature and runs the tests, build and linters.
- It creates a feature branch, commits only the files for this task and pushes.
- It starts two Codex reviews in parallel: a general one and a separate one for security.
- It reads the findings (labeled from P0 to P3), and verifies each against the code itself. It fixes the real ones and rejects the false ones, explaining why.
- It repeats while serious findings remain, but for no more than three rounds.
- It reports: what Codex found, what was fixed, what was rejected. Only after that do the changes reach the main branch, and it doesn’t open a PR without my command.
You get two models from different companies looking at the same code from different sides, with me as the last line of defense. Some of Codex’s findings are false positives, and Claude sees that on its own. But real findings do happen, and catching them this way is an order of magnitude cheaper than catching them in production.
What Happened Afterwards
About three and a half months have passed since the v1.0.0 release (July 11). Seven minor versions shipped, up to v1.7.0. During that time I added the MCP server, recurring transactions, a mobile app, email confirmation, rate limiting and billing. There were few bugs after the migration.
For comparison, by GitHub release publication dates: in all of 2025, PHP got 3 releases (v0.5.2, v0.6.0, v0.7.0). From January to May 2026 there were 5 (v0.8.0 to v0.10.1). After the migration, from July 10 to October 25, 20 releases shipped in three and a half months, from the v1.0.0 beta to v1.7.0.
Bottom line: for about a month of subscriptions (Claude $200, Codex around $25) I moved the whole project over and started shipping features more often and more effectively.
That is the main result for me. Cutting resource usage down to 14 MB instead of 250 is a nice bonus. What matters is that I can now develop the product very quickly, many times faster than before and with minimal bugs. Quality rests on: the compiler, tests, screenshots, package boundaries and the intelligence of the models.
Takeaways
- Choose technologies by the quality of feedback for the agent and by the effect on the product.
- Keep dependencies to a minimum. Every external dependency carries risk.
- When migrating, reproduce behavior first, and only then refactor.
- Package boundaries work as security boundaries.
- Tests are executable knowledge about the product: with them you can change the implementation entirely.
- Pay the closest attention to what is hard to reverse: migrations and external contracts.
- The human role shifts upward: what we build, where the risk is, what must not break.
The project is open source: github.com/econumo/econumo. The plans the agent worked from are in docs/superpowers, and the rewrites themselves were in PRs #46 (backend) and #58 (frontend).
If you want to keep a family or personal budget, try Econumo. There’s a free self-hosted option, and for those who don’t want to bother with installation, there’s a cloud version with a lifetime purchase. It’s one of the few products I’ve used myself on a regular basis for the last 5 years.