This has been a relaxing summer. Back in June, I decided to take a break from blogging after 77 weeks in a row of publishing a new article every Wednesday. I took some time off to recharge, spend time with my kids, and work on some projects I simply haven’t had the time to do because of all the blogging.
Early this year, I published a couple of articles on how I use ChatGPT to come up with workouts. Every day a Step Function workflow would run, ask ChatGPT to create a workout, then email it to me. I open-sourced the project and made it free to use on my website, with the hopes that people would use it. But they didn’t.
The project was too tightly coupled with how I work out. It was hardcoded with my preferences and used only equipment I had available in my home gym. Not very extensible to anyone who wasn’t me.
That’s when I saw the Amplify and Hashnode hackathon running in July. I thought this was the perfect opportunity to start over with nothing but lessons learned from my initial implementation. It also gave me an opportunity to learn about AppSync, try out the new GraphQL resource in SAM, use Amazon Cognito for the first time, and give my poor UI skills a little practice.
The reasons to participate were too good to pass up, so I dove in and built a product that can (and probably will) be a legitimate SaaS offering in less than a month. Let’s check it out.
Working out is hard enough as it is, but coming up with routines every day is even harder. Many people opt to pay for gym trainers who do it for a living - and that’s a great way to go. I used to be a personal trainer when I was in college, so I come up with the workout programs myself.
But I live on a farm, have two kids, a full-time job, and create content on Ready, Set, Cloud! as a side gig - I don’t have time to come up with training programs anymore.
So I built a Saas offering to do it for me. I configure which days I’d like to exercise, the types of workouts I like, what equipment I have available, and sit back and relax.
I receive an email the day before my workout to let me know what’s in store for the next day. Or if I’m not a fan of email, I can just log in to the app and view the workouts on demand. Simple enough!
Google Sign-On - I’m not a fan of websites that make you go through a lengthy signup process. So I made it a top priority to get auth federated through Google. When signing up, you can simply log in with your Gmail and be good to go in 2 clicks.
Multi-tenancy - My initial pass at this was single tenant… me. I had a public home page that shared the workout that was generated for me. As far as scaling goes, I reached the limit with just me. So I updated the project to be multi-tenant, meaning everyone can configure their own settings. Tenancy is done on an individual basis, using the Cognito user sub as the tenant id. Now anyone who logs in can set up their own preferences and get workouts tailored to their ability, goals, and available equipment!
High Configurability - There’s a fine balance of too much and not enough configurability. This workout app treads the line (and probably errs on the side too much) to offer the best workouts possible. You can set your fitness goals, experience level, favorite workout types, what muscle groups you want to work, what equipment you have available, how long you want to work out, what days of the week you want to exercise, and what time you want your workouts emailed to you.
AI-Powered Coaching - Using your personalized settings, the app asks ChatGPT-4 to create tailored workouts for you. You get a highly-customized, professional-quality set of workouts generated just for you. Your entire week of workouts is built every Sunday so you can see the training plan ahead of time.
SmartRandom* - Each workout is fed to the AI coach with random criteria, but in a way that prevents duplicate workouts. For example, if you want to work out 3 times a week and do arms one day, cardio the second day, and chest on the third day, the day of the week and utilized equipment is randomized, but muscle groups are not. Meaning you won’t end up working arms twice and chest once in a given week. I made sure to spread out the muscle groups throughout the week to ensure a balanced regime.
I would categorize this build into three distinct parts: the user interface, the workout generator, and the notification system.
The user interface is a Next.js app built using the AWS Amplify UI components for React. These controls wrap basic elements like
button and add styling to them. They also add some shortcuts and directives in these components to help drive behavior in a simple way. I would not have been able to build this app in the short amount of time I had if it weren’t for these components! No messing with CSS classes, just using the out-of-the-box styled components from Amplify.
The app uses Amazon Cognito to authenticate users via Google. This was probably the hardest part of the entire project - setting up and configuring the resources necessary to get a Cognito user pool wired up to Google. I kept the Cognito resources in a SAM template separate from the user interface to allow me to iterate on the back-end and UI separately.
I had to figure out what resources I needed, build them, then figure out how to manually create the config file Amplify uses to connect. You might be wondering why I didn’t use the Amplify CLI to do this since it does it for you automatically. Well, the answer is a tough pill to swallow:
You should not use the Amplify CLI to build production-grade applications.
It takes too many shortcuts and gives you access to do destructive things a little too easily. Since I want this app to eventually make its way to a true SaaS offering, I figured I’d start with the isolated resource stack so I wouldn’t have to unwind my progress after the hackathon was over.
Anyway, the UI also uses an AppSync GraphQL API which uses Cognito for auth. I was very happy with how easily the two integrated! I took advantage of the brand-new SAM resource for AppSync GraphQL APIs to see how easy it made development. Overall, I’m pretty impressed! My only cause for concern with it is that it might get a bit unwieldy if your API performs a bunch of queries and mutations. The YAML stacks up pretty quick when you define the functions and resolvers for so many actions.
People who follow my writing know I’m a big fan of Step Functions. So naturally, I orchestrated workout generation with a Step Function workflow.
This process is run on a recurring EventBridge schedule every Sunday at 6pm. It loads all the registered users, then iterates over each one of them, passing in their configured workout settings to a Lambda function that builds prompts to pass to ChatGPT.
Then it will pass the prompt to ChatGPT and provide a
responseSchema object to guarantee the format of the output. Once the workouts have been generated, the workflow creates a one-time EventBridge schedule to send the user an email with the workout information at their configured notification time.
Creating one-time EventBridge schedules was a big improvement over my initial design. Before I had the notification state machine run every day, look to see if there were workouts built for the future, and send an email if it found one. But I didn’t like that - it creates a lot of e-waste. The workflow was running when it didn’t need to be. I could do better.
The one-time schedule not only enabled me to limit the number of runs on the schedule, but it also allowed me to pass in the DynamoDB key for the workout record. Now I didn’t have to do a query and waste RCU’s over-fetching data. I could just get the single workout I wanted to work with and be done.
As you can see, this is once again a Step Function workflow. This one makes heavy use of direct integrations with various AWS services instead of using Lambda.
Two important things about this workflow to note:
If the user has email notifications turned off, then this workflow will never run. The workout emails are intended to tell users the day before their workout what to expect so they can be mentally prepared for what’s to come.
This was a great project. I had an idea and a first iteration built at the beginning of this year and learned a lot from it. But it’s not iteration if you only do it once. I rebuilt this application entirely from scratch. No code made it from the last iteration to this one.
We live in an age of “just do it for me.”
SaaS startups are popping up everywhere. If you want to do something, chances are there’s a SaaS offering for it already. People are ready to pay for you to do the hard work.
An interesting behavior I've noticed about myself is if a service is wrapped in a UI I'm more inclined to try it than if someone where to give me 100% of the source code and provide simple install instructions.— Focus Otter (@focusotter) July 24, 2023
As Michael Liendo says above, even when the source code is completely open and free to use, if you’ve stood up a user interface on top of it and can get it working, demand is there. With this in mind, here are my top lessons learned if you’re seriously considering building a SaaS product.
I’ll continue iterating on this design. The next big milestone is lifting this to its own domain and putting it behind a subscription model (it is SaaS, after all). But right now, you can access it and use it for free at fitness.readysetcloud.io or you can check out the source code on GitHub.
Upcoming features I’m excited about:
I’m excited for what’s to come and how this evolves. Incorporating a serverless back-end and using managed UI components for the front end have accelerated me faster than I ever thought possible. So thank you all for your support and (hopefully) your feedback!