Jakub Tkaczyk

Muscle Up

Strength & calisthenics coaching

I train calisthenics and street lifting, which means my sessions happen in a park or a specialized gym. The gym I attend has spotty signal. Every interesting workout app I tried assumed I’d have an internet connection to log a set. Additionally, I couldn’t really find an app that would give me decent tools to plan my trainings with street lifting details in mind. I value structured workouts, tracking the progress and being organised in the best way possible - I believe that’s how one can steadily progress. I was missing a reliable offline solution dedicated for my sport. So I built Muscle Up.

It’s a coaching platform for strength and calisthenics. Trainers build periodized programs, athletes follow them, log their work, submit form-check videos, and track PRs. But the part I keep coming back to is simpler than any of that - it stays instant, there is no loading state, and it keeps working when there’s no signal.

Why it’s local-first

Offline isn’t an edge case here, it’s more normal than one would think: a park in the woods, a gym on -1 level, reviewing next week’s block on the train - these are my usual scenarios. The app can’t pretend the connection is always there, so it stops depending on it.

Reads come straight from a local cache. Writes survive going offline and reconcile the moment one is back online. The sync engine underneath is what makes that hold up, and picking it was a whole decision of its own.

The coach-athlete loop

I’ve worked with great coaches in the past but in all cases, the cooperation lived in a mess of spreadsheets and DMs. Muscle Up pulls the program, the logging, and the feedback into one place - and keeps that place working even when the two people involved are not online at the same time.

A trainer builds a program the way periodization actually works: a program breaks into blocks, weeks, workouts, and every set is prescribed down to the detail: sets, reps, a tempo, a target RPE, a working weight, rest time. One trainer can run a whole roster like this. The team shares a single exercise library and a leaderboard, so a PR you hit in the gym lands next to everyone else’s.

Before the first set of the day, the athlete logs a quick readiness check-in: motivation, sleep, the upper and lower body fatigue from last workout. It feeds the part of the app I’m proudest of.

The Week 2 logging grid: readiness dropdowns (motivation, sleep, fatigue upper and lower) across the top, then each exercise with prescribed sets, reps, tempo, RPE and weight, an AI-suggested working weight beside it, a real-RPE input, and a video and note column.
A week of work, one row per exercise - prescription on the left, what actually happened on the right.

Each prescribed weight comes with a second number beside it: a suggestion. It starts from a proper estimated 1RM strategy (Epley, Brzycki or Lander), whichever the athlete prefers, then hands that, the readiness check-in, and the fatigue carried out of the last workout to an AI coach agent. It reads the whole picture and nudges the load up or down, with an explanation. Slept badly and legs still sore? It backs the squats off and informs about it.

Everything is logged offline - the work done, the real RPE for each set, a form-check video on the ones that mattered and any notes, thoughts the athlete had.

Back home, on stable WiFi, it syncs at once: the logs, the videos, the new PRs. The trainer picks it up, watches the form checks, leaves feedback, and adjusts next week. Neither person had to be online at the same moment for the loop to close. Workouts also flow outward as calendar events - the same feed my AI agents read when they plan my day.

How it’s wired

Muscle Up runs on two deliberate data paths, and most of the design comes down to deciding which path a given piece of data takes.

The first is Zero Sync: local-first sync for everything in the training domain. The client holds a replica and runs live queries against it, mutations apply optimistically, and a zero-cache service keeps the replica honest by reading Postgres logical replication. A shared sync-engine package holds the schema, queries and mutators, and runs the code on the client (optimistic) and the server (authoritative). It’s also where authorization lives: every synced query is scoped to the organization/team and role, so a client only ever pulls the data it’s allowed to see.

The second path is GraphQL, for everything Zero shouldn’t own. A Hono server hosts a GraphQL Yoga API handling auth, organization admin, user preferences, and presigned upload/download URLs for the form-check videos, which live in RustFS. Auth itself is Better Auth - OAuth, SSO, passkeys, and the multi-tenant organization model with a logical database separation.

The AI weight coach lives on this side too and it’s built with Mastra. It levereges Postgres for its memory, and ClickHouse records what it suggested and why.

It’s a small thing, and that’s the scope I wanted. I run a production version on a Home Lab cluster, and it’s used by me and a handful of friends from the street-lifting community.