I built a strength training app
My journey building a strength training app using expo

I'm a software engineer from Spain, building software and writing publicly about what I learn along the way.
I built a strength training application called Auxo using Expo, designed to help me stay consistent during my workouts.
As I usually do in personal projects, I kept a journal throughout the development process to document what I learned, the doubts I had, and the challenges I faced. In this article, I share that journal to tell the story behind building the app.
I hope you enjoy the read!
Motivation
Everyone knows exercise is good for your health, but honestly, I hate working out, especially strength training. That’s one of the reasons I prefer training at home.
To complete my sessions, I usually rely on two different apps: an interval timer for warm-ups and cooldowns, and my phone’s default clock app to track rest periods between exercises.
At some point, I started asking myself:
Why not build a single app that combines both?
Why not add a bit of gamification to make workouts more motivating?
And why not even encourage progressive overload to help users improve over time?
Goals
If you’ve read my previous article about building a VS Code extension, you probably already know my main rule when working on side projects: keep it simple.
I followed the same principle while building this app.
The main goal was straightforward: create something I would genuinely enjoy using during my workouts. To achieve that, I defined three key goals:
Visually pleasing, something nice to look at while training.
Easy workout creation, no friction and no unnecessary complexity.
A bit of gamification, motivation during workouts through a progress bar, and motivation across workouts through progressive overload.
The design
Before starting the project, I designed the application interface to better understand the features I wanted to implement. Below is the initial design I created using Figma.
As you can see, the features I wanted were:
Create, update, delete, and (obviously) start workouts.
Each workout should include a name and multiple sections, which can also be created, updated, and deleted.
Each section should include a name, a type, a rest time between exercises, and multiple exercises.
Each exercise should include a name, a type (time or reps), and a weight value.
All elements (workouts, sections, and exercises) should be draggable to allow free reordering.
During a workout, the app should display:
Time or reps, depending on the exercise type.
A progress bar.
The next exercise during rest periods.
A pause option for time-based exercises.
A “Next” button for rep-based exercises.
A progressive overload message after completing all sets in sections that are neither warm-ups nor cooldowns.
Of course, as you’ll see later, some features evolved over time, while others were refined or removed entirely. That’s simply part of the development process.
First steps
I actually had the idea for this project almost a year ago. I even tried to start it twice before, but neither attempt really went anywhere.
This time felt different, though.
I already had experience publishing a smaller project, a VS Code extension, and that gave me enough confidence to finally commit to building this app properly.
Those earlier attempts were not wasted either. They gave me some familiarity with React Native and Expo, which made getting started much easier.
I knew that running npx create-expo-app@latest would generate the initial project structure. From there, I configured Prettier properly, something I understood much better thanks to my previous project, and removed the default Expo boilerplate using the provided command npm run reset-project.
At that point, the project was finally ready to grow.
I started by building the main pages of the application: the homepage, workouts page, and sections page.
Database architecture
After implementing a few basic pages, I realized that before continuing with the application, I needed a backend. And before building the backend, I needed a solid database architecture.
So I started researching by reading articles, watching YouTube videos, and, of course, asking my AI friend ChatGPT (free plan).
One of the first decisions I had to make was choosing the database solution. After some research, I narrowed it down to two options:
SQLite for offline-first usage.
Supabase for cloud synchronization and online features.
Initially, I planned to use SQLite exclusively, but I already had ideas for features that would eventually require Supabase.
For a while, I wasn’t sure which direction to take. Then I asked myself: why not support both?
Once I introduced a dependency injection pattern, supporting both became a realistic option.
Even after making that decision, I chose to focus entirely on SQLite first and leave the Supabase implementation for the future. The final structure looked something like this:
database
├─ sqlite
│ ├─ db
│ ├─ init
│ ├─ workout
│ ├─ section
│ └─ exercise
├─ repositories
│ ├─ workout
│ ├─ section
│ └─ exercise
└─ hooks
├─ useWorkouts
├─ useSections
└─ useExercises
SQLite layer: handles the database logic directly.
Repositories: abstract the data layer and make future migration easier.
Hooks: consume repositories while remaining completely agnostic to the underlying database implementation.
From the beginning, I also thought about database migrations and how future app updates could affect the schema.
With some help from AI, I ended up implementing a simple solution that would allow the application to evolve safely over time.
Creating workouts
My original idea for workout creation was intentionally simple: a workout would contain a name and multiple sections of exercises.
Each section would contain several exercises, but also a specific section type. Depending on that type, the application would generate and execute the workout differently once the user started the session.
These were the section types I wanted the app to support:
Traditional: perform each exercise one by one with rest between sets.
Superset: alternate between two exercises with minimal rest.
Circuit: perform several exercises in sequence, then repeat the entire round.
Warm-up and cooldown: similar to traditional blocks, but treated separately inside the workout flow.
The goal behind supporting these different section types was to simplify workout creation while still allowing enough flexibility for different training styles.
That idea aligned closely with one of the main goals of the application: making workout setup as frictionless as possible.
Below, you can see the final version of the workout form. As shown in the screenshot and as I will explain later, I decided to rename “sections” to “blocks”.
And below, you can see the final version of the block form. As you can see, it has changed slightly from the original Figma design, where the workouts list is now shown horizontally instead of vertically.
Local state and Zustand
At the beginning, I built the application by interacting with the database every time a change was made, for example, whenever a workout was added or removed.
Pretty quickly, I realized this approach was not ideal.
What I actually needed was a large local state capable of representing the entire workout in memory. I decided to use Zustand for this, only persisting data to the database when the user explicitly saved the workout.
For the state design, I decided that each entity (workout, section, and exercise) should track its own status: created, updated, or deleted.
This made persistence much easier because, when saving changes, the application already knew exactly which database operation needed to be executed for each entity.
In the end, this approach simplified the UI logic, reduced unnecessary database operations, and gave me much better control over how and when data was persisted.
Main workout page
I have to admit I leaned quite a bit on AI while building the main workout page, especially for features like the progress bar and the workout generation logic, which depends heavily on the different section types.
That said, I made sure to fully understand the generated code and how the logic worked internally, so I still felt comfortable with the approach.
In the end, I was really happy with how the page turned out.
One feature I implemented entirely on my own was the countdown beep during the final three seconds of time-based exercises. It may sound simple, but adding small details like that made the app feel much more polished and motivating to use.
After introducing those features, I inevitably ran into a few bugs. Some were related to how supersets and circuit sections were generated, while others involved timing inconsistencies during workouts.
Fixing those issues helped me better understand the workout flow and timing logic, especially how different section types interact with one another.
Below, you can see the latest version of the workout page.
Refactor and dark theme
Toward the end of development, I shifted my focus toward improving the overall UI and UX of the application.
I built a custom toast component, improved the keyboard experience, added a contextual menu for different elements, and implemented a custom time input component.
AI tools accelerated much of this process, particularly during the more repetitive UI work.
Once most of the functionality was in place, I decided it was time for a larger refactor. I cleaned up the codebase, improved readability, and reorganized several parts of the project structure to make everything more consistent.
At the same time, I decided to add a dark theme.
For this part, I spent some time researching which colors work best in low-light environments. Interestingly, AI was not especially useful here, so I relied more on design resources instead.
I found this Figma community file particularly helpful, along with Material Design’s color system guidelines.
That research helped me create a dark theme that feels consistent, readable, and comfortable to use during workouts, especially at night.
Unit tests
I also decided to add unit tests... Yes! At the very end instead of the beginning.
In this area, AI was genuinely extremely useful.
With its help, I managed to build a solid test suite relatively quickly, and along the way, I even discovered a few bugs I had not noticed before.
More importantly, the experience reinforced something I already suspected: even late-stage testing provides a huge amount of value.
Choosing the name
At this point, I thought I was close to finishing the application development process, which, as you’ve probably realized by now, was not actually the case.
Still, I decided it was time to start thinking about a proper name for the app. I did not want to simply call it “Strength Training App.” I wanted something that felt more unique and memorable compared to other fitness apps.
At first, I asked ChatGPT for ideas.
That turned out to be a terrible approach.
Most of the suggestions sounded generic, and many of them already existed as app names. So instead of asking for random names, I started looking at how some successful fitness apps got their names.
The first example that came to mind was Strava.
After doing a bit of research, I learned that the name comes from the Swedish word sträva, which roughly means “to strive” or “to fight for something.”
I really liked that idea.
So I decided to follow a similar approach and started exploring names inspired by words from other languages with meanings related to growth, effort, or progress.
This time, the suggestions became much more interesting, and eventually one stood out to me the most: Auxo.
The name comes from the Greek word auxō, meaning “to grow” or “to increase,” which felt perfect for a workout-related application focused on progression and self-improvement.
Once I had the name, I also designed a simple icon for the app based on the letter “A.”
Of course, right after all of that, I realized something important:the app was still far from finished.
Early feedback
At this point, I had an MVP of the application, so I decided it was time to think seriously about publishing it.
I paid for access to the Google Play Console and generated an APK for testing purposes. That allowed me to share the app with friends, and the feedback I received ended up shaping the project far more than I expected.
One friend who regularly goes to the gym really liked the idea and started suggesting features that actually sounded realistic to implement. Some of them included:
Being able to increase or decrease reps and weight directly during workouts.
A more advanced exercise configuration system with customizable sets.
Better UI/UX overall, especially since several friends agreed that reordering elements using arrows felt terrible.
At that point, I realized that, even if it contradicted my “keep it simple” rule, it was probably time to expand the scope of the project a little.
After all, with AI tools helping me, how difficult could it really be?
New features
At first, progress was incredibly fast.
While I still had free Copilot tokens available, I managed to implement the sets feature, improve large parts of the UI, and replace the old arrow-based reordering system with drag-and-drop cards.
To improve the UI further, I also decided to adopt React Native Reusables. That decision required changing a large number of components and eventually forced me to refactor the theme-switching system once again.
Eventually, though, the free tokens ran out.
I didn’t want that to slow down development so I shifted my focus toward fixing existing issues and implementing smaller improvements manually. During this phase, I added features like adjusting reps and weight directly during workouts, along with a dedicated settings page.
I also renamed “sections” to “blocks” because I realized I preferred thinking about workouts as blocks of exercises rather than isolated sections.
Of course, as often happens during development, introducing all these new features also introduced new problems.
The theme broke again, several unit tests stopped working, and a few parts of the UI became inconsistent.
Gym, doubts and decisions
A few months later, encouraged by some friends, I decided to start going to the gym regularly instead of training exclusively at home.
That experience completely changed how I viewed the app.
At the gym, I found myself using my phone much less and focusing more on the exercises themselves. Instead of relying on my app, I naturally gravitated toward using my smartwatch for rest timers and a simple Excel sheet to track progress.
That realization led me to ask myself some uncomfortable questions:
If I no longer use the app during my gym workouts, why should I finish it?
And if AI can generate an app in a single day now, why should I continue building mine?
The strange part is that these doubts appeared when the application was already close to completion.
Even so, I decided to keep going.
Not because the app was guaranteed to succeed, but because the project itself still mattered to me.
Maybe I no longer use the application as much as I originally expected, but that does not mean other people would not find it useful.
More importantly, my goal was never only to build a product. It was also to learn.
Through this project, I learned about React Native, Zustand, SQLite, mobile architecture, theming, testing, and the entire publishing process around mobile applications.
Yes, I probably could have built the app faster by relying even more heavily on AI tools. But if speed were the only objective, I would lose one of the main reasons I enjoy building projects in the first place.
So I made the decision to finish and publish the app anyway, even if nobody ends up using it, not even me.
New type of block
As I mentioned earlier, one of my main goals was to make workout creation as simple and flexible as possible through different block types.
However, after joining a gym, I realized something important: many times I ended up changing the order of exercises during a workout simply because a machine was already being used.
That led me to a simple question:
Why not let the user decide which exercise to do next during the workout itself?
With that idea in mind, I decided to introduce a new block type: Flexible.
Unlike the other block types, Flexible blocks do not enforce a fixed exercise order. Instead, they allow users to choose which exercise to perform next during the workout session.
It was a relatively small feature, but it completely changed how adaptable the app felt in real gym environments.
Below, you can see the latest version of a flexible workout.
Finding bugs
After reaching what I considered a relatively stable version of the application, I generated another APK and started testing it again.
This time, instead of asking friends for feedback, I decided to approach testing more systematically by applying the experience I have gained in my current role as an Automation QA Engineer.
After several hours of testing, I discovered multiple issues that I considered important enough to fix before publishing the app. Some of them included:
Validation errors when navigating back from workout or block forms.
Saving issues when modifying reps and weight values during workouts.
Problems with the light theme not rendering correctly.
Various UI inconsistencies across the application.
During this phase, I also decided to add a help section inside the settings page so users could contact me directly if they encountered bugs or issues.
Publishing
To publish the application on the Google Play Console, I first had to complete several required questionnaires and provide all the necessary store listing assets. This included writing both a short and a long description of the app, creating screenshots for mobile phones and tablets, and designing a feature graphic.
In addition, Google requires developers to provide a Privacy Policy. To meet this requirement, I created and published a dedicated Privacy Policy page using Notion.
Once the initial setup was completed, I prepared the first release and submitted it to the Closed Testing track. Before requesting access to the Production track, Google Play requires new developers to meet specific testing requirements. In my case, the application had to be tested by at least 12 testers over a period of 14 consecutive days during the closed testing phase.
At the beginning, I thought I could ask my friends for help, but then I realized it would be difficult to ensure that all twelve of them would actively install and use the app enough to meet Google’s requirements.
After reflecting on the situation, I decided to take a simpler approach by making the project open-source and releasing the APK on GitHub instead.
The result
In the end, I achieved my main goal: I built a strength training application that genuinely feels useful to me.
Of course, there are still features I would like to improve or expand, especially around gamification and progressive overload, both of which were part of the original vision for the app.
Still, I see this as only the first version.
I already have several ideas for future updates, including workout import/export functionality and other quality-of-life improvements that I did not have time to implement yet.
For now, though, I am happy with where the project ended up. It feels like the foundation of a much more polished workout app that could eventually become useful not only for me, but for other people as well.
Below you can see a GIF of the final version of the application.
Conclusion
I have to admit that throughout this project, I relied on AI more than I originally expected.
That made me reflect quite a bit on how much I should depend on it while building software. After finishing the project, my conclusion is that the answer depends heavily on the type of project you are working on and the goals behind it.
In my case, I build projects mainly for two reasons: to solve problems I personally face and to learn new technologies.
For example, the VS Code extension I wrote about in a previous article solved a real problem for me at the time, even if I do not use it as much anymore. The same happened with this workout app. I originally expected to use it during every workout session, and for the most part, that has been true.
At the same time, I also build projects to learn and to keep up with how quickly technology evolves.
That naturally led me to another question:
Am I still learning if I use AI throughout the process?
I want to believe the answer is yes.
Before starting this project, I had never:
Thought seriously about database migrations.
Built a mobile app with Expo.
Used SQLite in a real project.
Worked with Zustand.
Implemented light and dark themes in an application.
Tried to publish an app on the Play Store (even though, in the end, I couldn’t complete this step, I still learned from the experience).
Sure, I am far from an expert in all those technologies, and I probably would have learned more deeply without AI assistance.
But without AI, this project also would have taken significantly longer, and there is a real chance I might never have finished it at all.
As I mentioned in my previous article, writing about the development process helps me reflect on what I learned and better solidify that knowledge afterward.
So even if the app itself never becomes particularly successful, I still consider the project a success from a learning perspective.
Thanks for reading!
I hope this article was both insightful and entertaining, and maybe even encouraging enough to inspire you to start building something of your own.
If you’d like to explore the code, you can find it on GitHub. And if you want to try the application, you can download the APK.
