Agon: Race Against the Ghost

Inspiration

Running is one of the most accessible and popular forms of exercise in the world. But running alone can be difficult to sustain. Motivation drops when there’s no competition, no shared experience, and no real-time feedback beyond distance or pace.

Existing apps like Strava focus heavily on tracking performance after the run, but the run itself remains repetitive. Any group function is limited, there is no competition, and the community dies - our own high school group fizzled out after a week.

We wanted to explore a simple idea:

What if every run felt like racing someone?

This led us to Agon, a platform where runners can compete asynchronously by racing against each other live. However, we realised that not everyone has the same schedule - what if you were free at different times? Thus, the ghost feature was born. Record a time, and have your friend race your ghost whenever they are free. Instead of running alone, you’re always chasing someone.

We extended this to our whole leaderboard, introducing an elo system that ranks users on a global scale, allowing the true athletes to rise the ranks through global ghost matchmaking.

What It Does

Agon is a mobile application that turns solitary runs into competitive races. Users can:

  • Record Runs: Track their route, distance, and pace in real-time.
  • Race Ghosts: Select a friend's past run or match with a similarly ranked runner globally and race their "ghost" asynchronously.
  • Climb the Leaderboard: Earn Elo points by beating opponent ghosts, rising through a global ranking system to prove your athletic prowess.

Built With

  • Frontend: React Native, Expo, Tailwind CSS, Uniwind, HeroUI Native
  • Backend: Convex
  • Architecture: Turborepo (Monorepo)
  • APIs & Tools: React Native Maps, Expo Location

How We Built Our Project

Defining Project Requirements

We started by outlining the key idea behind Agon — turning every run into a race. From there we defined the essential features such as ghost racing, asynchronous competition, and a global leaderboard system.

Architecture and Setup

We structured the project using a Turborepo monorepo, allowing us to share code and manage separate workspaces for the native mobile app and backend.

Building the Backend

Using Expo and Convex documentation and AI tools, we implemented the backend logic for storing runs. By polling GPS data at a 5-second interval, the distance covered between each timestamp is calculated entirely from scratch using the Haversine Distance Formula:

$$d = 2R \arcsin\left(\sqrt{\sin^2\left(\frac{\phi_2-\phi_1}{2}\right) + \cos(\phi_1)\cos(\phi_2)\sin^2\left(\frac{\lambda_2-\lambda_1}{2}\right)}\right)$$

Building the Frontend

We built the React Native application using Tailwind CSS with Uniwind, alongside HeroUI Native components. This was heavily supported by the hot-reloading feature within the Expo Go mobile app, which enabled incredibly fast iteration speeds.

Using the timestamped location data fetched during the run, we integrated React Native Maps and Expo Location to track user movement in real-time, displaying their route directly on the map as they run.

Testing and Debugging

Throughout development, we continuously tested the application using Expo development builds, device simulators on Xcode, and the Expo Go app to ensure everything worked seamlessly across different devices.

Challenges Faced

Day 1 Communication Breakdown On the first day, we experienced a major communication failure between the frontend and backend workflows. Because we were moving so fast, developers were building UI components that expected a specific data structure, while the backend was implementing a completely different schema in Convex. We lost valuable hours rewriting data fetching logic and refactoring components. It forced us to halt development, jump on a quick whiteboard session, and strictly agree on our API contracts before writing any more code.

Middleware and Environment Variables Syncing On Friday night, we got stuck with creating new middleware routes. The functions were showing up in Convex fine, but the frontend just wasn't finding them. Because we were working within a Turborepo across both the frontend and backend, our environment variables weren't being shared properly. We eventually realised the whole issue boiled down to not having the same Convex URL across our different stacks. Once we synced up the env vars, it was all sorted.

Expo Version Mismatches on iOS On day two of the hackathon, the backend was mostly dusted, but we ran into a bit of trouble trying to test the app on our iPhones using Expo Go. It turns out the components feature we were relying on was recently added in Expo 55, while the official Expo Go app only supports up to Expo 54. To get around this, we used a development build and managed to add Expo 55 support onto the Expo Go app through Apple TestFlight. We also made sure we could simulate the app via Xcode on our laptops by prebuilding and building the iOS and Android React Native application.

Overly Strict Repository Setup Early on, our initial repo setup was also way too strict. We had pre-commits set up that would block us over minor linting warnings, which really bogged down our development speed. We ended up just ditching the pre-commit hooks altogether, which took a lot of the frustration out of the process and let us actually get on with coding.

Accomplishments That We're Proud Of

  • Shipping a Cross-Platform App: Successfully building and deploying a functional iOS and Android application under severe time constraints as a team of first-year university students.
  • Custom Mathematics: Implementing the Haversine formula from scratch to accurately calculate distance using raw GPS polling data rather than relying entirely on pre-built distance tracking libraries.

What You Learned

Setting Strict Boundaries As a first-year university team tackling UNIHACK, setting strict boundaries right from the start was honestly the only reason we managed to finish right at the buzzer.

React Native and Expo are No Joke Building our first mobile app gave us a major wake-up call regarding cross-platform compatibility. It’s wild seeing your code work flawlessly on one platform and completely break on another.

Good Git Practices are a Lifesaver By communicating constantly about what PRs were getting merged and sticking to proper rebasing, we somehow managed to keep our main branch perfectly healthy with zero downtime.

Built With

  • biome
  • bun
  • convex
  • expo.io
  • nextjs
  • tailwind
Share this project:

Updates