← Back to all projects

2025 · Everything — design, frontend, API, schema

Body Level

Calisthenics progression as a skill tree

ReactTypeScriptRedux ToolkitExpressMySQLJWT
Body Level interface

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.