I love the mountains. Every single one of them. And I try and make as much time as life allows it to spend time in the mountains. (rvnd.me/stories)

I cannot count the number of times trail apps like AllTrails have saved my bacon when I've gone off trail. Without them I would not feel nearly as confident while out in the wilderness.

However, almost every time I made critical decisions like when to turn around, it has always been a mix of subjectiveness and real-world signals. Curveballs like a sketchy section that keeps getting sketchier, or a sudden turn in weather are all par four course in the mountains.

What decides if you have to change course or turn-back aren't just these challenges, but rather a key situation factor - available time. With enough time, one can carefully navigate the sections, or take shelter to see the weather through.

But not knowing how long it will take at your current situational pace to complete the hike brings a significant wild card into the mix. Almost every time I found myself in this situation, acclaimed apps failed to give me reliable information.

Surely there has to be a better way!

It started with a good question

The original idea was born from a frustration I’ve had with popular hiking apps: they are good at telling me where I have been, but not particularly good at telling me what I care about while I’m still out there.

How much farther do I have to go?

How much climbing remains?

At my current pace—not some generic hiking pace—when will I reach the destination?

And more importantly, when will I get back to the trailhead?

I wanted those answers to continue working without cell service. The initial Alberta focus made that requirement fairly obvious. Signal disappears quickly once you leave the highway, and an app that discovers its offline limitations after the hike begins is not really an offline app.

The idea soon expanded into an offline mountain companion. In addition to route progress and return time, it would identify nearby peaks, rivers, waterfalls, passes and lakes. It could calculate a conservative turnaround time before daylight became a problem. A narration mode would announce useful information while the phone stayed in my pocket. Over time, the app could learn my climbing and descending pace instead of treating every hiker as a fixed constant.

Each feature made sense on its own.

Together, they smuggled in an enormous number of assumptions.

By the time I assembled the project context for a coding agent, I was no longer describing one product question. The document covered MapLibre, PMTiles, OpenStreetMap, Canadian geographical names, terrain models, adaptive ETA, fatigue, route deviation, narration, emergency summaries, downloadable regions and data provenance.

I called the app SummitLine.

Then I started building.

Building the intelligent part first

The pace model was one of the most interesting parts of the project, so I went fairly deep on it.

Naismith’s Rule gives a basic estimate using distance and ascent. Tobler’s hiking function goes further by modelling how walking speed changes with grade. Both are useful baselines, but neither knows anything about me.

SummitLine’s model was intended to learn from recorded hikes. It separated stoppage from moving time, considered climbing and descending pace independently, adjusted for fatigue, and recalibrated its forecast from the portion of the hike already completed.

Conceptually, the forecast looked something like this:

remaining time =
    sum of time across every remaining trail segment

segment time =
    segment distance
    ÷
    grade-adjusted personal pace
    ÷
    fatigue adjustment
    ÷
    terrain adjustment

The app would also compute the return separately. It would not simply double the time to the summit, because a return trip can include different grades, rolling terrain, slower technical descents and a hiker who is several hours more tired than when they started.

There was real care in this part of the design. SummitLine had deterministic route calculations, confidence states, stoppage detection, route-deviation tracking, daylight calculations, personal pace storage and held-out evaluation for recorded hikes.

It also had a reproducible offline fixture for Lady Macdonald. The fixture contained real route geometry, elevation samples, terrain tiles, named features and a synthetic GPS replay. I could run a hike without leaving my desk and watch the location advance along the line.

And it worked.

That last sentence needs an asterisk.

It worked against a route I had already selected, cleaned, packaged and understood.

A downloaded line is an attractive abstraction

The early SummitLine model treated a downloaded trail as an ordered polyline:

Trailhead A → B → C → D Destination

Once the user’s GPS location was projected onto that line, progress appeared straightforward:

progress = distance from A to the projected position
remaining = total route distance - progress

The same position unlocked the rest of the experience. Look ahead along the elevation profile to calculate remaining ascent. Apply the pace model to the remaining segments. Reverse the route for the return estimate. Compare the result with civil twilight to calculate a turnaround time.

It was elegant because one position along one line answered almost everything.

It was also brittle for exactly the same reason.

If I travelled from D toward A, my projected distance from A decreased even though I was moving forward in my actual journey. SummitLine could correctly say, “You are on this geometry,” while incorrectly interpreting what that match meant.

The app had location confidence without journey understanding.

That distinction did not feel important while replaying a synthetic GPX file in one direction. On Ridgeback, it was impossible to ignore.

Ridgeback Trail Loop was not one trail

Before the field test, getting Ridgeback into SummitLine had already exposed a data problem.

There is no single OpenStreetMap way or relation in Kananaskis named “Ridgeback Trail Loop.” What exists around Station Flats are four separately mapped sections named Ridgeback R1 through R4. They belong to a connected trail network with junctions, alternate approaches and other paths.

The name I knew referred to a hiking experience. The source data described pieces of physical infrastructure.

Those are not the same thing.

Rather than fabricate a loop that OpenStreetMap did not define, I generated and published all four real Ridgeback sections. That was the honest data decision. But the app still treated each downloaded section as though it represented the user’s intended journey.

It did not know which sections I planned to combine. It did not know which direction I intended to travel. It did not know whether I would return along the same path, complete a loop, take a connector or leave the predefined route altogether.

A map tells me what exists.

A trail network tells me where movement is possible.

A route describes what I intend to do.

A hike is what I actually end up doing.

I had compressed all four into a downloaded line.

The online pipeline was clever. Too clever.

SummitLine had another practical problem: a trail was not useful offline until it was on the phone. I did not want every trail to require a developer to build a new fixture and ship another version of the app.

So I built live trail search and download.

The system looked roughly like this:

iPhone search
    ↓
Cloudflare Worker
    ↓
OpenStreetMap Overpass — route and nearby geometry
    ↓
Open-Meteo — elevation
    ↓
Cloudflare Container running Tippecanoe — PMTiles
    ↓
Cloudflare R2 — cached downloadable bundle
    ↓
iPhone

The first request for a new trail generated its bundle on demand. Later requests received the cached version from R2.

This was satisfying when it worked. A hiker could search for a real Alberta trail, trigger generation from public data and download a complete offline package without rebuilding the app.

But consider what had to happen before the offline experience could begin. The Worker needed a healthy Overpass mirror. Elevation had to be fetched. A container might need to start. Tippecanoe had to generate the map archive. Every artifact then had to be stored and downloaded.

An offline app had developed a surprisingly elaborate online ceremony.

Overpass soon began throttling or timing out requests from Cloudflare’s shared outbound addresses. The same query could fail repeatedly from the Worker while completing in seconds from my own machine. I added retries, mirror fallback and client-side timeouts. Broad Alberta searches were reduced to park-scoped searches so each query covered a smaller bounding box.

The changes helped, but they could not remove the structural dependency.

During one outage, I generated the Ridgeback bundles locally and uploaded them manually to R2. That unblocked the field test. It should also have been a clue: the expensive map-curation work was more reliable on a development machine than in the hiker’s search request.

I was treating batch data preparation as a live product feature.

What the field test actually tested

I wrote the Ridgeback observations under a heading called “Bugs.”

That was accurate, but incomplete.

One issue was ordinary application state. Leaving the trail screen and returning caused the visible controls to disagree with the active hike session. The screen and the hike controller did not have one dependable source of truth.

Another was lifecycle state. Completing a hike left the next Start Hike action unresponsive.

The broken ETA, distance and elevation looked like a map-data problem at first. Some of it probably was. But the reverse-traversal behavior exposed a domain-model problem underneath it: even perfect elevation samples would not help if the app misunderstood which way I was travelling and what route I intended to complete.

Then there was the infrastructure problem. Creating a new offline trail depended on several live services behaving well at the same moment.

These failures belonged to different layers:

UI state                 → “Start Hike” during an active session
Session lifecycle        → unable to begin again after finishing
Source-data quality      → questionable distance and elevation inputs
Route model              → confident match, confused progress
Acquisition architecture → slow and unreliable live generation

Grouping them all as “bugs” made the work look like a repair list.

The field test was really an architecture review.

That is one of the most useful things I carried away from SummitLine: test outside the controlled fixture early enough that the result can still change the design. A simulator replay is excellent for verifying calculations. It cannot tell you whether the calculation represents the problem a hiker actually has.

Moving the complexity away from the trailhead

My field notes immediately pointed toward a different data pipeline: map curation should become an asynchronous job that runs largely offline.

The rule became blunt:

Stop generating anything live.

Instead of asking a Worker to construct a trail bundle while someone waited, a local pipeline could acquire a whole region, inspect its trail network, combine data sources, generate elevation and map assets, validate the result and upload it once.

The proposed flow became:

Acquire a regional extract
    ↓
Discover candidate trails and connected segments
    ↓
Curate what belongs in the pack
    ↓
Build the trail graph, elevation and map assets
    ↓
Validate checksums, coverage and connectivity
    ↓
Review
    ↓
Publish once to R2
    ↓
Let every phone download the reviewed result

This was not merely a cost optimization.

Moving the work offline created room for judgment.

OpenStreetMap discovery could tell me that 57 ways were connected. It could not tell me that all 57 belonged in a hiking product. Some might be mountain-bike routes. Some might have seasonal closures. Some might carry difficult alpine classifications that deserved review before appearing as an ordinary trail.

The pipeline could produce a human-readable discovery report, followed by a curated selection file. Raw discovery would never publish itself.

The same regional terrain acquisition could support elevation profiles, contours and hillshade. A full trail graph could preserve junctions and connectors instead of flattening the landscape into one route per package. Named peaks, waterways, huts and passes could be curated for the whole region rather than whatever happened to fall within a narrow query around one trail.

Most importantly, a questionable pack could fail before it reached the phone.

That is what offline-first had been missing from my thinking. Offline-first is not only about whether the app makes a network request during a hike. It is also about doing uncertain, expensive and failure-prone work somewhere it can be inspected—long before the hiker depends on the result.

SummitLine had answered too many questions at once

The Ridgeback notes also contained the beginnings of a “Take Me Home” feature. If a user left a downloaded route, the app should use whatever offline network remained available to find a path back toward the starting point.

That idea was closer to the real problem. It acknowledged that hikers leave clean polylines and that a return journey may need to be inferred from a network.

But SummitLine was still carrying the full weight of the original mountain-companion vision: rich maps, route discovery, personal pace, narration, surrounding geography, turnaround safety, emergency information and general navigation.

I could continue repairing each piece. There was enough sound engineering in the project to make that tempting.

But what was I actually trying to make dependable?

Ridgeback gave me a fairly unforgiving answer. The map could be beautiful. The route match could be confident. The architecture could use interesting technology. None of that mattered when the return estimate was wrong.

The next version did not begin with another feature.

It began by making the question smaller:

Given where I am, how long until I am back at the trailhead?