Ready, Set, Go! A multi-player triathlon timing app built with Elixir and Phoenix
In the summer of 2024, I built an app to time a triathlon that I helped organize at a company retreat. I called it Ready, Set, Go! and built it using the Elixir language and Phoenix framework. Here are some notes about it.
Demo
- The actual results from the Camunda triathlon are at https://ready-set-go.gigalixirapp.com/track/1. This event is locked, and all you can see are the results.
- There is a demo event at https://ready-set-go.gigalixirapp.com/track/2. This event is active. You can see athlete times change when someone updates them (see next point).
- If you log in with the credentials
readysetgo@example.com/letsrace!!!!!, you’ll be able to advance/roll back athletes. This is how we were timing the race — myself and two recruits were all logged in, and tapping the “Advance” button when someone reached a specific point on the course. The “Oops!” button was just in case we tapped the wrong button (which I did at least once). - The app updates times live, and pushes those updates to every client via websockets. If you open the tracker in two separate tabs, you can see this in action by advancing an athlete in one tab. We as timers had live updates from each other throughout the race. We could each tell where everyone was on the course by looking at the app. Observers, who were not logged in, could see live updates of athletes progressing through the race.
Feel free to play around with the demo event. While building & testing, I spent a lot of time clicking athletes forward and backward through the race in one authenticated window, and observing updates in a second incognito window.
Note that my hosting plan is on the free tier, and occasionally the app turns off due to inactivity. If that happens, bother me and I'll turn it back on!
The code
- Source: https://github.com/pepopowitz/ready-set-go/. I think I have enough described in the README to get it running locally, if that’s something that interests you.
- The app is built with the Phoenix framework, which is Elixir-based. It was my first time building something with Phoenix or Elixir. I have poked at Elixir enough to be able to understand it when I look at it. Phoenix is very similar to Rails, and my Rails experience came in very handy. I enjoyed the experience and the language/framework very much.
- Phoenix has first-class support of websockets through a feature it calls LiveView. This is the feature that made me want to write this app with Phoenix, because I knew I would need multiple people timing the athletes, and I wanted live updates for everyone.
- The app is styled using Tailwind.
- I initially thought I needed a React front-end to make such an interactive app. As I learned more about Phoenix and LiveView, I discovered that was not at all the case. There is almost zero JavaScript written by me. The only JavaScript I wrote was the hook that keeps the timers running every second. (It seemed like a bad idea to update from the server every second 😅😬)
- It’s messy, and has some dead-ends, but I kept a log of how I was learning & building things at https://github.com/pepopowitz/ready-set-go/blob/main/HISTORY.md.
- Once I’d gotten started, I built features basically by asking GitHub Copilot to do it for me. It was usually correct enough for me to just drop the response into my source. I’ve had struggles with CoPilot in regards to things like writing bash scripts in the past, where I would get a lot of nonsense answers and have to rework 15 times. This was the opposite experience, where almost everything worked, and it enabled me to build at least 10 times faster.
- I did zero automated testing because I was moving fast. Don’t tell anyone. The only tests that are there were generated by Phoenix, and I highly doubt they still pass.
The race
- After finding out the location for the 2024 Camunda retreat would have a lap pool, cycling room, and running track, I and another triathlete agreed to organize a mini triathlon for Camundi to participate in. We thought it would be fun to time the app so people could get competitive with each other, and also have something to show others for their accomplishment.
- The race was to be a low stakes and low commitment event, accessible to any athletes who wanted to try.
- There were other events at the same time, and we didn't want to force people to feel obligated to do the race.
- We did not want to introduce specialized timing equipment. It would complicate the event, and potentially turn people off from participating by making them nervous. (Nor did we have the budget...)
- We chose shorter distances than most triathlons - 200m swim, 5km bike, and 2km run.
- Timing the race was not a primary goal. We wanted people to have fun and learn about the sport that Miklas and I both loved (and both burned out on later that year 😅). If the timing could not be done simply, we would not do it.
- There were many timing challenges introduced by things like the layout of the resort and the day's schedule.
- The pool was located near the cycling room, but still required a 200m transition from swim to bike. This meant the timer at the swim could not also be the timer for the bike.
- The lap pool only had two lanes, and we had ~30 people interested, so we could not have everyone start their swim at the same time.
- The best we could do is a serpentine format, where swimmers start in a time-trial format, a few seconds apart from each other. Each side of each lane is one direction only, and swimmers zig-zag their way from right-to-left (or left-to-right) at the end of each length.
- The bike room contained 8 stationary bikes. We had to restrict the number of athletes on that part of the course, and we chose to do so by releasing athletes in waves.
- We did not want the running course to finish at the pool, for fear of someone slipping. We also did not want to finish it at the bike room, as it was an inconvenient location for fans. This meant that the timer at the end of the run could not also be the timer for the bike, or the timer for the swim. We're up to 3 timers.
- With other events happening at the same time, some athletes preferred to start early, and some preferred to start late. This combined well with our waved start.
- We had no idea what the WIFI would be like at any location on the course. We would either need to time without an internet signal, or pick specific timing locations where the WIFI was good.
Why I built the app
- There are services out there to time a race without specialized equipment, and we could have easily paid $10 to use one of them to track the event. Of the handful of services I looked at, I did not find one I was happy with. Some were expensive, some had a challenging UX, some would have introduced unwanted friction into the event.
- I saw the opportunity to build something that would solve a problem, to my preferences, and get some hands-on experience with the Elixir language and Phoenix framework. The multi-player aspect seemed like a good opportunity to use the LiveView feature of Phoenix specifically.
Requirements for the app
- Timing should be multi-player. Athletes would be finishing a discipline in three different locations, so we needed three different people to be able to time the same athlete.
- The timing app should be easy to use on a mobile device, for ease of use.
- It should be easy to advance an athlete to the next discipline, and easy to undo it in the event of a mistake.
- Anyone should be able to see the position and status of an athlete at any time. Only the timers should be able to move the athlete from one discipline to another.
- It should be easy to find an athlete in the app while timing them.
- Times should update every second, so we can see an accurate elapsed time for everyone.