Hood to Coast Athlete Tracker

Making a nearly 200-mile relay a little less chaotic.

Hood to Coast is a 12-person relay where runners rotate through dozens of legs while two vans leapfrog their way from Mt. Hood to the Oregon coast.

After racing it, one problem kept bothering me: we were surprisingly bad at knowing when the next handoff was actually going to happen.

Sharing someone’s location helped, but a dot on a map didn’t answer the question everyone actually cared about:

When is our runner getting here?

Role Designer, developer, team captain, runner

Tools
Claude, VS Code, GitHub Copilot, Notion, FigJam

Platform
iPhone web app (PWA)

Timeline
July - August 2026

Tech stack
Next.js, Supabase, Vercel, Leaflet, OpenStreetMap, Traccar

Status
Raced once. Iterating for 2027.

Why I made it

Hood to Coast is the largest relay race in the world, run every year since 1982. It starts at Timberline Lodge on Mount Hood, runs through Portland and the Coast Range, and finishes on the beach in Seaside. The course is split into 36 legs. Each of a team's twelve runners takes three, and the race runs through a full day and an overnight.

Most teams split into two vans that leapfrog down the course. Van 1's six runners take their legs while Van 2 eats, rests, and drives ahead to the next major exchange. Then they swap, and the cycle repeats until every leg is done. Every handoff happens at an exchange, and every exchange depends on the next runner being there at the right moment, often in the dark, and often somewhere with no cell service.

Learn more at the official race website.

12

runners

36

legs

2

vans

197

miles

11 of 12

runners tracked live

28

hours to finish

Three jobs at once

I was the team captain, one of the twelve runners, and the only person building the app. As captain, I kept the schedule and the logistics together. As a runner, I spent part of the race out on the course, away from my phone. As the builder, I was responsible for a system the whole team depended on, while doing the other two jobs.

That overlap shaped the product more than anything else. I couldn't design something that needed me watching it. It had to work while I was running, sleeping, or somewhere without signal, and when it needed a person to step in, that had to be quick enough to do from a van between legs.

The problem wasn’t location. It was timing

This was our second year running the race, and the project came straight out of the first. In year one, we did what most teams do and shared iPhone locations. We could open Find My and see roughly where our runner was. It felt useful, but it didn't answer the question that mattered.

A dot on a map tells you where someone is. It doesn't tell you when they'll get to the exchange. To work that out, someone had to check the map, guess how far the runner was from the exchange, estimate their pace, and do the math, usually while tired and in a moving van. Different people came up with different answers, and a wrong answer meant a runner standing in the cold for twenty minutes, or a van showing up after its runner had already arrived.

The schedule stops being accurate almost immediately. Every team starts with a schedule. You take each runner's expected pace, multiply it by their leg distances, and get a predicted time for every handoff. It's a good plan for about the first hour. Then someone has a great leg, someone else hits a long climb, and every handoff after that moves. By the overnight legs, the printed schedule is mostly guesswork.

The problem wasn't location. It was timing: turning where the runner is into when they'll arrive, and keeping that accurate as the race drifts away from the plan.

What I designed

Most running apps are built around one athlete. This one was built around a team. It's a relay operations app, not a fitness tracker.

Who it was for

The first real design insight was about who the app was for. The obvious user of a runner-tracking app is the runner. But the runner is busy running. They already know where they are and roughly how fast they're going. The people who needed information were everyone else. Each of them needed something slightly different.

The next runner

Needed to know when to start warming up, when to leave the van, and when to be standing at the exchange. Too early means cooling down in the cold. Too late means missing the handoff.

The driver and the van

Needed to know how long they had before moving. Is there time to get food, find parking, or get to the next exchange ahead of the runner?

Everyone else

Needed to know whether they could sleep, eat, or change, and where the team stood in the race overall. Especially the other van, which could be miles away and completely out of the loop.

Why the existing tools fell short. Before designing anything, I collected screenshots of the tools we already used, plus fitness and tracking apps I admired, on a FigJam board. Seeing them side by side made the gap obvious. We weren't short on tools. None of them answered the timing question.

Find My and shared locations show where someone is, but not how far they are from the exchange or when they'll get there. Strava and Garmin are excellent at tracking one athlete: pace, splits, routes, and effort. But they're built for the person running, not for eleven teammates waiting on them. The printed schedule has the right structure, every leg and every handoff in order, but it can't adjust once the race drifts from the plan.

Each tool covered part of the problem. What was missing was something that combined all three: live location, each runner's pace, and the structure of the race, turned into one clear answer anyone on the team could act on.

Now and next

The whole product came down to one mental model: what's happening now, and what happens next. A relay moves as one repeating sequence:

runner → leg → exchange → next runner → next leg

At any moment, one leg is active. That leg anchors the whole live experience. Everything else in the app follows from knowing who is running it and how far along they are. The main screen had to answer the team's questions in one glance, from a passenger seat, on a bumpy road, often at night. It's split into two halves.

NOW
  • Who's running

  • Which leg is active

  • Where they are on the course

  • How far through the leg they are

NEXT
  • When they'll reach the next exchange

  • Who's running next

  • Where the team stands in the overall relay

The screen leads with the ETA and the next runner, because those are what change what people do. The next runner starts warming up, the driver starts moving, and everyone else decides whether they have time to sleep.

From coordinates to a handoff time. The ETA is the core of the product. It combines the runner's live location, the course and leg distance, their pace, and how far they've already come. Coordinates alone aren't useful to anyone in a van. Instead of this:

Runner 8 · 45.4102, -122.2941

The app says this:

Runner 8 · 72% through Leg 8 · Exchange in ~14 min

The map is context, not the answer. A map is the obvious centerpiece for a tracking app, and it's the wrong one here. Reading a map means working things out yourself: how far is that dot from the exchange, and how fast is it moving? The app does that math and shows the answer. The map is still there when someone wants to see the route, but it's supporting detail, not the headline.

The timeline: the plan versus the real race. The timeline replaced the printed schedule. It shows completed legs alongside upcoming ones, with handoff estimates that shift as the race does. When one runner has a fast leg, every handoff after it moves earlier, and the whole team can see the change without anyone recalculating by hand.

Designing for missing data

When GPS disappears. I knew location would drop out, so I built a fallback. Every runner had a planned pace. When live location went quiet, the app kept estimating progress from the leg's start time, its distance, and that runner's pace. In effect, it said:

I don't know exactly where the runner is. Based on when they started and how fast they run, this is roughly where they should be.

When a fresh location came back, the estimate snapped back to real data. The team didn't have to do anything different. The answer just got more or less precise. The plan and the live state. To make all of this work, the app keeps two kinds of information separate: the plan the team starts with, and the live state that changes as the race goes on.

THE PLAN
  • 12 runners and their leg assignments

  • 36 legs with known distances and routes

  • Exchange locations

  • Each runner's expected pace

  • Planned exchange times

THE LIVE STATE
  • Which runner is active

  • Which leg is active

  • When that leg started

  • The runner's latest location, when there is one

Every estimate in the app comes from combining the two. With a fresh location, the live state leads. When the location goes quiet, the plan fills the gap. That split made the pace fallback possible, and it turned out to matter even more on race weekend.

What I left out. Deciding what not to build mattered as much as what I built. Strava and Garmin already do these things well, and none of them helps 12 people across two vans know when to be where:

  • Social features and feeds

  • Chat

  • Leaderboards and rankings

  • Deep running analytics like splits, heart rate, and elevation

If a feature didn't help someone get to the right place at the right time, it didn't ship.

How I built it

Built to ship, not to demo. This wasn't a prototype. It was a working app, and twelve people were going to depend on it for 28 hours. That set the bar for every build decision.

A link, not an App Store listing. I built it as a progressive web app. Everyone on the team had an iPhone, so setup meant opening a link in Safari and choosing "Add to Home Screen." From then on it opened like a native app, full screen with its own icon. No App Store review, no TestFlight invites, and no chasing twelve people to install a build the week before the race. Each runner's phone reported its location in the background, so once they were set up, runners didn't have to do anything during the race. Under the hood, it runs on Next.js, with Supabase handling the database and live data and Vercel handling hosting. The map is built on open-source mapping tools, so there were no licensing costs for a one-weekend project.

Building with an AI team

I was the only person on the project, but I didn't work alone. I split the work into three lanes. Claude was my strategy and spec partner. It helped plan architecture, write specs, and push back when I was about to do something risky. Claude Code in VS Code did the implementation. GitHub Copilot handled small interface tasks that couldn't touch the core logic. The speed came from the tools. The quality came from the rules I set around them:

  • Every request to Claude Code was a complete, standalone prompt with all the context it needed.

  • Every task started with an audit: read the relevant code and report back before changing anything.

  • The fragile parts of the system (race state, ETA math, handoff logic, the map) were off-limits unless they were the actual target.

  • Every change went through a branch, a pull request, and my review. Nothing was committed straight to the main branch.

  • Layout changes weren't done until I'd checked them on a real iPhone.

The AI wrote most of the code. I owned every merge, 60+ pull requests of them. The workflow around the AI. AI tools are only as good as what you give them, so two other tools held the project together. Notion was the source of truth for the work. Every bug and every upcoming feature lived there as its own item, so nothing depended on my memory or on a chat thread I'd scrolled past. Each Claude Code session started from a Notion item and ended with that item updated. When I sat down to build, I knew exactly what was next and what was still broken.

FigJam was where I talked about the interface visually. Beyond the inspiration board, it became my bug-reporting tool. When something looked wrong on my phone, I'd drop the screenshot into FigJam and mark it up with arrows, circles, and notes on exactly what should change. Then I'd bring the annotated image back into Claude Code with reference notes. Describing a layout bug in words is slow and easy to misread. Showing it is fast and precise.

screenshot on iPhone → annotate in FigJam → back into Claude Code → fix → check on iPhone

It's the same way I'd work with an engineer on a team: point at the problem, don't just describe it.

Getting it race-ready

Checking the real thing. The last stretch before the race was less about new features and more about making sure what was running matched what I thought was running. Twice, checks said everything was fine when it wasn't: once when the deployed version had quietly drifted from the code, and once when a clean merge still broke the app. Both times, the only way to catch it was to look at live data and use the real app. A few days before the race, I also removed the testing simulation entirely. Leftover test data had come close to interfering with the live race, and taking the feature out was the safest change I made all month.

The failsafe. The night before the race, I built one more thing: a manual override that let me set the active runner and leg directly. If the app's picture of the race ever went wrong, I could correct it. The app would then pick up that runner's location when it came through and push the corrected race state to every phone. I hoped not to need it. It turned out to be one of the most important things I built.

Using it for real

There was no controlled test environment. The race itself was the test. Eleven of us used the app for 28 hours, from the start at Timberline to the finish in Seaside: through Portland, into the Coast Range, overnight, while running, driving, and sleeping in shifts.

What worked

The core idea held up. Instead of asking "where's our runner?", the team could open the app and see who was running, how far along they were, and when they'd reach the exchange. Next runners timed their warm-ups off the ETA, and the vans planned stops around it. The other van, often miles away, could follow the race without a stream of texts.

The pace fallback worked better than I expected. Runners lost signal constantly, and every time, the app kept estimating from their planned pace instead of freezing. A dropped signal made the estimate a little less precise, not useless.

Where it broke

Connectivity was the biggest constraint, and the race made it clear that in this environment it isn't an edge case. It's the normal condition. Losing a runner's GPS mid-leg was survivable. Losing service at an exchange wasn't. A relay runs on handoffs: one runner finishes, the next starts, and every estimate after that depends on the new state. If nobody has signal when a handoff happens, the app keeps the previous runner "active" while the real race has already moved on. Automatic handoff detection was supposed to handle those transitions. When exchanges had no service, the automatic handoff couldn't register.

The failsafe earned its place. The override I'd built the night before is what kept the race on track. When the app's picture of the race fell behind, I corrected the active runner and leg from the van, and the app pushed the fix to every phone. Keeping the plan and the live state separate paid off here. Only the live state was wrong, so fixing it took seconds and nothing else had to be rebuilt. The same flexibility helped when the race didn't follow the plan. When a runner ran without a phone, I set their pace manually so the estimates kept working. When two runners traded legs mid-race, I reassigned them and the app followed.

It was a recovery tool, not the experience I ultimately want. But it's the reason the app stayed useful all the way to Seaside.

What I learned

Going in, I thought I was building a prediction tool. The question I started with was simple: how do I predict when the runner will reach the exchange? Pace, distance, and live location all fed into that answer, and on a clear stretch of road with a strong signal, it worked. Coming out of the race, I realized I'd been working on a different problem all along: how do I keep a distributed team in sync when the network itself can't be trusted? Predicting an arrival time is fairly easy when data is flowing. The hard part is keeping twelve people, split across two vans and often miles apart, with the same correct picture of the race when the network drops out at exactly the moments that matter.

The biggest lesson was that I'd gotten the design constraint backwards. I designed for connectivity as the normal case and treated dropouts as exceptions to handle. The race had it the other way around. Through the Coast Range and overnight, no signal was the default, and the app worked because the fallbacks carried it, not because the live data did. The race isn't getting better cell service next year. The product has to get better at working without it.

The second lesson was about recovery. The manual override was the last thing I built, the night before the race, almost as insurance. It turned out to be the feature that kept everything else working. Any system that makes decisions on its own, like deciding a handoff happened or estimating an arrival, will eventually get something wrong, and the most important question is how quickly and safely a person can correct it. The third lesson was about testing. No amount of testing at home would have shown what happens at an exchange with no signal at 3 a.m. One weekend of real use taught me more than months of building at home.

What comes next

The next version treats poor or no connectivity as a core requirement from day one, not something to patch later. That means keeping the race state on each phone so the app keeps working offline and syncs when signal comes back. It means confirming handoffs at the exchange itself, so the active runner can be determined without depending entirely on the server. And it means recovering on its own when a runner's location reappears, so the race doesn't need me correcting it from a van. I also want the ETA to show how confident it is, so the team can tell a solid estimate based on fresh GPS from a rough guess based on pace alone.

The goal isn't to eliminate uncertainty. A relay through rural Oregon will always involve some guessing. The goal is for the app to understand its own uncertainty and communicate it clearly, so the team always knows how much to trust what it's seeing.

That's also why this project leads the Making page. There was no brief and no stakeholder. I hit a problem during something I love, the existing tools didn't solve it, and I built something. Then I used it for real, watched my assumptions break, and learned what the actual problem was. That's the kind of work I want to keep doing: finding the real problem by building something and putting it in front of people who depend on it.