2025 · Everything — design, frontend, API, schema
Body Level
Calisthenics progression as a skill tree

The problem
Calisthenics is one of the few kinds of training where progression is genuinely structured: you cannot do a one-arm push-up until you can do a normal one, and a pull-up sits on top of a row. The dependencies are real, not motivational.
Every app I looked at flattened that into a checklist. A list of exercises tells you what exists; it does not tell you what you have earned or what is next. The structure that makes calisthenics legible was the exact thing the tooling threw away.
So the question was whether the dependency graph could be the interface rather than something buried in an article — and whether unlocking the next movement could feel like the reward it actually is.
How I approached it
Model the prerequisites as data, not as copy
The schema has a dedicated skill_prerequisites table alongside skills and user_skill_progress. Prerequisites are edges in a graph, which means the tree is generated from the database rather than hardcoded in the UI, and adding a movement is a row rather than a component. Six tables in total, covering users, profiles, sessions, skills, prerequisites and per-user progress.
Let progress drive what the UI reveals
A skill's state is derived from whether its prerequisites are satisfied for that user, so locked, available and completed are computed rather than stored as flags that can drift. Redux Toolkit holds the tree state; category filters across Push, Pull, Legs and Core narrow the graph without refetching.
Build the smallest real version first
It started as a single skill tree with XP attached and nothing else — no auth, no profiles, no achievements. Getting one path working end to end told me whether the core idea was actually satisfying to use before I spent time on the surrounding product. Everything else got added once that felt right.
Own the backend rather than reaching for a BaaS
I wrote the API in Express with TypeScript against MySQL, with JWT sessions, bcrypt password hashing and cookie-based auth. Using a hosted backend would have been faster, but I wanted to write the schema, the queries and the auth myself — the point of the project was partly to stop treating the server as someone else's problem.
What it's built with
- Frontend
- React, TypeScript, Redux Toolkit, React Router, Material-UI, Emotion, Framer Motion
- Backend
- Express, TypeScript, MySQL (mysql2), with a separate server tsconfig and build step
- Auth
- JSON Web Tokens, bcryptjs password hashing, cookie-parser, CORS
- Data
- Six-table MySQL schema — users, user_profiles, user_sessions, skills, skill_prerequisites, user_skill_progress
What I'd do differently
- The API and the client live in one repository with separate TypeScript configs, and the deployed build does not run the Express server. The hosting story should have been decided before I wrote the backend, not after.
- A .env file went into the repository. Nothing in it is live — local host, empty password, placeholder secret — but it should never have been committed, and .gitignore should have covered it from the first commit.
- There are no tests around the prerequisite logic, which is the one piece where a bug is invisible: a wrong edge does not crash anything, it just quietly unlocks something too early.