A Cyprus motorsport workshop was running its customers, vehicles, jobs and invoices through a hand-rolled PHP application on a single database file. It worked, but only from a desk, and nobody could safely change it.
The challenge
The data mattered more than the code: years of clients, vehicles, invoices and payments had to arrive in the new system complete, with totals that still added up. A rewrite that loses a paid invoice is worse than the tool it replaces. The second requirement was the workshop floor, because quotes and invoices get written next to the car rather than at a computer. The third was that one person maintains all of this, so deployment had to be boring.
What we built
One application rather than three: a Next.js monolith serving both the pages and a versioned JSON interface, PostgreSQL under a typed schema and migration tool, and a shared package of validation schemas so the web client, the server and the mobile client all agree on the shape of a vehicle, an invoice and a payment. Clients, vehicles, jobs, invoice generation and payments all live there.
For the floor, a native Android client in Kotlin and Compose, signed and installed directly rather than through a store, talking to the same interface. Invoice drafts survive being interrupted, which is what actually happens when you are holding a phone under a bonnet.
Authentication is argon2 hashing with short-lived access tokens and rotating refresh tokens; presenting one twice revokes the whole family, on the assumption that a replayed token is a stolen one. The browser keeps its tokens in cookies JavaScript cannot read, the app in encrypted storage, and the database is not exposed to the host.
The migration is a script rather than an afternoon of manual work. It reads the old file, writes the new database, then prints a reconciliation of row counts and money totals and exits with an error if source and target disagree.
The outcome
The replacement is built and runs on staging, and the Android client points there deliberately: the production domain still serves the legacy PHP application, and the cutover has not happened. The migration is written and reconciles, but it has not yet been run for real.
What exists today is the whole flow working end to end on staging - a job raised at the car, an invoice produced from it, a payment recorded against it - and a release path where merging updates staging by itself while production waits for a person to approve it. That is what turns the cutover into a decision rather than an event.

