AI and agents6 filesInfrastructure2 filesDevelopment5 filesAbout4 files

TrainerPad — a CRM for personal trainers that works offline and resolves its own sync conflicts

  • offline
  • sync
  • IndexedDB

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.