TrainerPad — a CRM for personal trainers that works offline and resolves its own sync conflicts
TrainerPad — a CRM for personal trainers that works offline and resolves its own sync conflicts
Personal trainers manage clients and sessions from the gym floor, where the connection is either absent or unreliable. A standard PWA with a cache doesn’t solve that: once two devices diverge offline the data has to be reconciled, and “last write wins” here means a lost training session.
I built the product alone, from scratch: 626 commits over six months, roughly 67,000 lines of TypeScript. React with Vite and Zustand, Dexie over IndexedDB on the client, Express with Prisma and PostgreSQL on the server.
The sync engine
At its core is a custom engine, 646 lines. The change queue lives in IndexedDB with its own merge
rules: a create followed by an update collapses into a create, an update followed by a delete
collapses into a delete — otherwise the server receives operations on a record it has never seen.
The sync loop runs every 30 seconds and fires immediately when a tab regains focus. Two open browser
tabs are kept apart by a navigator.locks guard; without it they race to push the same queue. The
server pull uses a two-second overlap and favors local changes: the overlap covers clock drift, the
preference guarantees that a fresh edit made in the gym is not overwritten by the server’s reply.
The client balance is recalculated locally rather than trusted from the server.
Before that the project used PowerSync. I compared it with RxDB, ElectricSQL and Turso and migrated to Dexie plus plain REST, removing about 6,300 lines of third-party runtime. This was not a fight with a library on principle: the set of conflicts in the trainer scenario turned out to be narrow and specific, and describing it in my own rules was cheaper than bending a general solution.
What that looks like in practice: at one point I had to restore 154 exercises for a user out of their own local database after a failure. For cases like that the product has a forced resync and an export of the local database into Telegram.
Everything else, also alone
The product runs in production on my own server: HTTPS with security headers, an admin panel alongside it, and self-hosted GlitchTip for error monitoring.
Search engines get the single-page app pre-rendered: the prerender step lives in CI, where Chrome on a runner walks the public routes and writes out ready HTML with structured data. The keyword map is described in code, next to the routes.
The blog is written semi-automatically: on a schedule a headless agent prepares drafts and cover images, and I edit and publish them. One person runs the product, the infrastructure, the SEO and the content machine at the same time.