I designed, built, and continue to maintain and develop new features for this product myself as a side project, since March 2023. The product idea came about because a personal trainer was using a variety of different apps to run her business, and wanted to consolidate them into one with only the features she wants. I continue to work closely with this user to understand her real problems and needs, and provide value by figuring out solutions and developing features that she wants and benefits from. As this is a side project, I have to be ruthless about working only on stuff that the user cares about.
To help me ship faster, I use GitHub Actions to automate CI code quality checks, backups, and production database migrations. When I merge a pull request into the main branch, it is automatically deployed on Vercel, where I have analytics and logging. And I use Sentry for error monitoring so I can fix things when they break.

Features
The core feature of the app are shared calendars between the personal trainer and each trainee. The trainer is able to view every trainee's calendar, and create, update, and delete appointments, workouts, and bootcamps. Trainees can only view their own calendar, including detailed information for workouts and appointments, check off workouts as completed, and check off bootcamps to confirm attendance if they have enough credits.
Other features include a mobile-friendly view for the trainees, tables where the trainer can view billing data and email invoices with a single click, and custom forms managed in Contentful CMS that get emailed to the trainer when a trainee fills the form out.

Iterations
With the first prototype, I experimented with Next.js 13 and the brand new App Router which was still quite unstable at the time. I decided to use RTK Query for data fetching and caching, and Redux Toolkit for state management. In hindsight, I over-engineered the state management by using Redux, as the app does not need complex client-side state. I recently did a big refactor to remove Redux, and experiment with moving everything to the server, removing the need for global client state, and relying on Next.js caching features.
However, I wasn't happy with the performance and user experience of dynamic route segments rendered at request time. And the scale and dynamic nature of the app means that it isn't feasible to prerender the routes at build time. So I refactored again to a mix of server and client-side rendering, but without global client-side state, and instead using React Query to manage server state.
Data is prefetched in Server Components and cached, so it is immediately available on the client. Data for adjacent calendar months is then prefetched on the client and cached, so they load immediately when the user navigates to them in a Client Component. React Query is also used to invalidate cache when users update server state, and to synchronize other users' clients with the updated server state. This makes the user experience very snappy.